diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index b2b08bab54..3266eb396a 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -786,6 +786,13 @@ def trunk_delete(request, trunk_id): neutronclient(request).delete_trunk(trunk_id) +@profiler.trace +def trunk_show(request, trunk_id): + LOG.debug("trunk_show(): trunk_id=%s", trunk_id) + trunk = neutronclient(request).show_trunk(trunk_id).get('trunk') + return Trunk(trunk) + + @profiler.trace def network_list(request, **params): LOG.debug("network_list(): params=%s", params) diff --git a/openstack_dashboard/api/rest/neutron.py b/openstack_dashboard/api/rest/neutron.py index 215ff0614c..8e742c0d75 100644 --- a/openstack_dashboard/api/rest/neutron.py +++ b/openstack_dashboard/api/rest/neutron.py @@ -146,6 +146,12 @@ class Trunk(generic.View): def delete(self, request, trunk_id): api.neutron.trunk_delete(request, trunk_id) + @rest_utils.ajax() + def get(self, request, trunk_id): + """Get a specific trunk""" + trunk = api.neutron.trunk_show(request, trunk_id) + return trunk.to_dict() + @urls.register class Trunks(generic.View): diff --git a/openstack_dashboard/dashboards/project/trunks/urls.py b/openstack_dashboard/dashboards/project/trunks/urls.py index 5413fbc770..15206e820d 100644 --- a/openstack_dashboard/dashboards/project/trunks/urls.py +++ b/openstack_dashboard/dashboards/project/trunks/urls.py @@ -21,4 +21,6 @@ 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/static/app/core/openstack-service-api/neutron.service.js b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js index ac15cd6dc1..e613590278 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js @@ -46,6 +46,7 @@ getQosPolicy: getQosPolicy, getQoSPolicies: getQoSPolicies, getSubnets: getSubnets, + getTrunk: getTrunk, getTrunks: getTrunks, updateProjectQuota: updateProjectQuota }; @@ -54,6 +55,15 @@ ///////////// + // NOTE(bence romsics): Technically we replace ISO 8061 time stamps with + // date objects. We do this because the date objects will stringify to human + // readable datetimes in local time (ie. in the browser's time zone) when + // displayed. + function convertDatesHumanReadable(apidict) { + apidict.created_at = new Date(apidict.created_at); + apidict.updated_at = new Date(apidict.updated_at); + } + // Neutron Services /** @@ -369,6 +379,27 @@ // Trunks + /** + * @name getTrunk + * @description + * Get a single trunk by ID + * + * @param {string} id + * Specifies the id of the trunk to request. + * + * @returns {Object} The result of the API call + */ + function getTrunk(id) { + return apiService.get('/api/neutron/trunks/' + id + '/') + .success(function(trunk) { + convertDatesHumanReadable(trunk); + }) + .error(function () { + var msg = gettext('Unable to retrieve the trunk with id: %(id)s'); + toastService.add('error', interpolate(msg, { id : id }, true)); + }); + } + /** * @name getTrunks * @description @@ -379,6 +410,11 @@ function getTrunks(params) { var config = params ? {'params' : params} : {}; return apiService.get('/api/neutron/trunks/', config) + .success(function(trunks) { + trunks.items.forEach(function(trunk) { + convertDatesHumanReadable(trunk); + }); + }) .error(function () { toastService.add('error', gettext('Unable to retrieve the 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 020435735d..feeed9849c 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 @@ -132,6 +132,15 @@ 42 ] }, + { + "func": "getTrunk", + "method": "get", + "path": "/api/neutron/trunks/42/", + "error": "Unable to retrieve the trunk with id: 42", + "testInput": [ + 42 + ] + }, { "func": "getTrunks", "method": "get", diff --git a/openstack_dashboard/static/app/core/trunks/details/details.module.js b/openstack_dashboard/static/app/core/trunks/details/details.module.js new file mode 100755 index 0000000000..e2dfd864cb --- /dev/null +++ b/openstack_dashboard/static/app/core/trunks/details/details.module.js @@ -0,0 +1,56 @@ +/* + * 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. + */ + +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @ngname horizon.app.core.trunks.details + * + * @description + * Provides details features for trunks. + */ + angular + .module('horizon.app.core.trunks.details', [ + 'horizon.framework.conf', + 'horizon.app.core' + ]) + .run(registerTrunkDetails); + + registerTrunkDetails.$inject = [ + 'horizon.app.core.trunks.basePath', + 'horizon.app.core.trunks.resourceType', + 'horizon.app.core.trunks.service', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function registerTrunkDetails( + basePath, + trunkResourceType, + trunkService, + registry + ) { + registry.getResourceType(trunkResourceType) + .setLoadFunction(trunkService.getTrunkPromise) + .detailsViews.append({ + id: 'trunkDetailsOverview', + name: gettext('Overview'), + template: basePath + 'details/overview.html' + }); + } + +})(); diff --git a/openstack_dashboard/static/app/core/trunks/details/overview.controller.js b/openstack_dashboard/static/app/core/trunks/details/overview.controller.js new file mode 100755 index 0000000000..d9e9528fc1 --- /dev/null +++ b/openstack_dashboard/static/app/core/trunks/details/overview.controller.js @@ -0,0 +1,72 @@ +/* + * 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. + */ + +(function() { + "use strict"; + + angular + .module('horizon.app.core.trunks') + .controller('TrunkOverviewController', TrunkOverviewController); + + TrunkOverviewController.$inject = [ + 'horizon.app.core.trunks.resourceType', + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.openstack-service-api.userSession', + '$scope' + ]; + + function TrunkOverviewController( + trunkResourceTypeCode, + registry, + userSession, + $scope + ) { + var ctrl = this; + + ctrl.resourceType = registry.getResourceType(trunkResourceTypeCode); + ctrl.tableConfig = { + selectAll: false, + expand: false, + trackId: 'segmentation_id', + /* + * getTableColumns here won't work as that will give back the + * columns for trunk, but here we need columns only for the + * subports, which is a (list of) dictionary(ies) in the + * trunk dictionary. + */ + columns: [ + {id: 'segmentation_type', title: gettext('Segmentation Type'), + priority: 1, sortDefault: true}, + {id: 'segmentation_id', title: gettext('Segmentation ID'), + priority: 1, sortDefault: true}, + {id: 'port_id', title: gettext('Port ID'), priority: 1} + ] + }; + + $scope.context.loadPromise.then(onGetTrunk); + + function onGetTrunk(trunk) { + ctrl.trunk = trunk.data; + + userSession.get().then(setProject); + + function setProject(session) { + ctrl.projectId = session.project_id; + } + } + } + +})(); diff --git a/openstack_dashboard/static/app/core/trunks/details/overview.controller.spec.js b/openstack_dashboard/static/app/core/trunks/details/overview.controller.spec.js new file mode 100644 index 0000000000..7ccca6f169 --- /dev/null +++ b/openstack_dashboard/static/app/core/trunks/details/overview.controller.spec.js @@ -0,0 +1,76 @@ +/* + * 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. + */ + +(function() { + 'use strict'; + + describe('trunk overview controller', function() { + var ctrl; + var sessionObj = {project_id: '12'}; + var neutron = { + getNamespaces: angular.noop + }; + + beforeEach(module('horizon.app.core.trunks')); + beforeEach(module('horizon.framework.conf')); + beforeEach(inject(function($controller, $q, $injector) { + var session = $injector.get('horizon.app.core.openstack-service-api.userSession'); + var deferred = $q.defer(); + var sessionDeferred = $q.defer(); + deferred.resolve({data: {sub_ports: [{'port_id': '1', 'seg_id': 2}, [], {}]}}); + sessionDeferred.resolve(sessionObj); + spyOn(neutron, 'getNamespaces').and.returnValue(deferred.promise); + spyOn(session, 'get').and.returnValue(sessionDeferred.promise); + ctrl = $controller('TrunkOverviewController', + { + '$scope': {context: {loadPromise: deferred.promise}} + } + ); + })); + + it('sets ctrl.resourceType', function() { + expect(ctrl.resourceType).toBeDefined(); + }); + + it('sets ctrl.trunk.sub_ports', inject(function($timeout) { + $timeout.flush(); + expect(ctrl.trunk).toBeDefined(); + expect(ctrl.trunk.sub_ports).toBeDefined(); + expect(ctrl.trunk.sub_ports[0]).toEqual({'port_id': '1', 'seg_id': 2}); + })); + + it('sets ctrl.trunk.sub_ports if empty array', inject(function($timeout) { + $timeout.flush(); + expect(ctrl.trunk).toBeDefined(); + expect(ctrl.trunk.sub_ports).toBeDefined(); + expect(ctrl.trunk.sub_ports[1]).toEqual([]); + })); + + it('sets ctrl.trunk.sub_ports if empty object', inject(function($timeout) { + $timeout.flush(); + expect(ctrl.trunk).toBeDefined(); + expect(ctrl.trunk.sub_ports).toBeDefined(); + expect(ctrl.trunk.sub_ports[2]).toEqual({}); + })); + + it('sets ctrl.projectId', inject(function($timeout) { + $timeout.flush(); + expect(ctrl.projectId).toBe(sessionObj.project_id); + })); + + }); + +})(); diff --git a/openstack_dashboard/static/app/core/trunks/details/overview.html b/openstack_dashboard/static/app/core/trunks/details/overview.html new file mode 100755 index 0000000000..9bcf4f9f4d --- /dev/null +++ b/openstack_dashboard/static/app/core/trunks/details/overview.html @@ -0,0 +1,19 @@ +
+ + +

{$ ::'Subports' | translate $}

+
+
+ + +
+
diff --git a/openstack_dashboard/static/app/core/trunks/summary.html b/openstack_dashboard/static/app/core/trunks/summary.html index 263b2c1b31..2556ac7da7 100644 --- a/openstack_dashboard/static/app/core/trunks/summary.html +++ b/openstack_dashboard/static/app/core/trunks/summary.html @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/openstack_dashboard/static/app/core/trunks/trunks.module.js b/openstack_dashboard/static/app/core/trunks/trunks.module.js index 6eb8f73eeb..cc0b99e6e4 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.module.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.module.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2017 Ericsson * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -30,6 +30,7 @@ 'ngRoute', 'horizon.framework.conf', 'horizon.app.core.trunks.actions', + 'horizon.app.core.trunks.details', 'horizon.app.core' ]) .constant('horizon.app.core.trunks.resourceType', 'OS::Neutron::Trunk') @@ -58,7 +59,8 @@ .append({ id: 'name_or_id', priority: 1, - sortDefault: true + sortDefault: true, + urlFunction: trunksService.getDetailsPath }) .append({ id: 'port_id', @@ -156,6 +158,14 @@ $routeProvider.when('/project/trunks', { templateUrl: path + 'panel.html' }); + + $routeProvider.when('/project/trunks/:id', { + 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 dcb5ce47c4..1e13f43255 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.service.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.service.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2017 Ericsson * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -22,7 +22,9 @@ trunksService.$inject = [ 'horizon.app.core.openstack-service-api.neutron', - 'horizon.app.core.openstack-service-api.userSession' + 'horizon.app.core.openstack-service-api.userSession', + 'horizon.app.core.detailRoute', + '$location' ]; /* @@ -35,12 +37,26 @@ * but do not need to be restricted to such use. Each exposed function * is documented below. */ - function trunksService(neutron, userSession) { + function trunksService(neutron, userSession, detailRoute, $location) { return { - getTrunksPromise: getTrunksPromise + getDetailsPath: getDetailsPath, + getTrunksPromise: getTrunksPromise, + getTrunkPromise: getTrunkPromise }; + /* + * @ngdoc function + * @name getDetailsPath + * @param item {Object} - The trunk object + * @description + * Given a Trunk object, returns the relative path to the details + * view. + */ + function getDetailsPath(item) { + return detailRoute + 'OS::Neutron::Trunk/' + item.id; + } + /* * @ngdoc function * @name getTrunksPromise @@ -56,6 +72,25 @@ return neutron.getTrunks(params); } } + + /* + * @ngdoc function + * @name getTrunkPromise + * @description + * Given an id, returns a promise for the trunk data. + */ + function getTrunkPromise(identifier) { + return neutron.getTrunk(identifier).then(getTrunkSuccess, getTrunkError); + + function getTrunkSuccess(trunk) { + return trunk; + } + + function getTrunkError(trunk) { + $location.url('project/trunks'); + 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 75f177af4d..bd2cfdf428 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2017 Ericsson * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -26,6 +26,19 @@ service = $injector.get('horizon.app.core.trunks.service'); })); + describe('getTrunkPromise', function() { + it("provides a promise", 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); + var result = service.getTrunkPromise({}); + deferred.resolve({data: {id: 1, updated_at: 'May29'}}); + $timeout.flush(); + expect(neutron.getTrunk).toHaveBeenCalled(); + expect(result.$$state.value.data.updated_at).toBe('May29'); + })); + }); + describe('getTrunksPromise', function() { it("provides a promise that gets translated", inject(function($q, $injector, $timeout) { var neutron = $injector.get('horizon.app.core.openstack-service-api.neutron'); diff --git a/openstack_dashboard/test/api_tests/neutron_rest_tests.py b/openstack_dashboard/test/api_tests/neutron_rest_tests.py index bb98d2b14d..246e28b3f3 100644 --- a/openstack_dashboard/test/api_tests/neutron_rest_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_rest_tests.py @@ -167,6 +167,16 @@ class NeutronTrunkTestCase(test.TestCase): neutron.Trunk().delete(request, 1) client.trunk_delete.assert_called_once_with(request, 1) + @mock.patch.object(neutron.api, 'neutron') + def test_trunk_get(self, client): + trunk_id = TEST.api_trunks.first().get("id") + request = self.mock_rest_request(GET={"trunk_id": trunk_id}) + client.trunk_show.return_value = self.trunks.first() + response = neutron.Trunk().get(request, trunk_id=trunk_id) + self.assertStatusCode(response, 200) + client.trunk_show.assert_called_once_with( + request, trunk_id) + class NeutronTrunksTestCase(test.TestCase): diff --git a/openstack_dashboard/test/api_tests/neutron_tests.py b/openstack_dashboard/test/api_tests/neutron_tests.py index 353a4a4a2e..e5cd0efa70 100644 --- a/openstack_dashboard/test/api_tests/neutron_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_tests.py @@ -430,6 +430,17 @@ class NeutronApiTests(test.APITestCase): for t in ret_val: self.assertIsInstance(t, api.neutron.Trunk) + def test_trunk_show(self): + trunk = {'trunk': self.api_trunks.first()} + trunk_id = self.api_trunks.first()['id'] + + neutron_client = self.stub_neutronclient() + neutron_client.show_trunk(trunk_id).AndReturn(trunk) + self.mox.ReplayAll() + + ret_val = api.neutron.trunk_show(self.request, trunk_id) + self.assertIsInstance(ret_val, api.neutron.Trunk) + def test_trunk_object(self): trunk = self.api_trunks.first().copy() obj = api.neutron.Trunk(trunk) diff --git a/releasenotes/notes/bp-neutron-trunk-ui-72e05888e68502c4.yaml b/releasenotes/notes/bp-neutron-trunk-ui-72e05888e68502c4.yaml new file mode 100644 index 0000000000..b1dd6d0ddd --- /dev/null +++ b/releasenotes/notes/bp-neutron-trunk-ui-72e05888e68502c4.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + [`blueprint neutron-trunk-ui `_] + Add partial support for Neutron Trunks. Since the panel is + incomplete in Pike, it is disabled by default. An example 'enabled' + file is supplied. After enabling it the Project/Network/Trunks + panel turns on 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. + Currently supported actions: delete.