Trunks panel: details for trunks and subports

Add details link to the trunk panel to show details of the given trunk,
like the subports list, with port_id and segmentation details.

Co-Authored-By: Bence Romsics <bence.romsics@ericsson.com>
Change-Id: I9000e2907a8f188d5e72e36818b7171f35158eb9
Partially-Implements: blueprint neutron-trunk-ui
This commit is contained in:
Lajos Katona 2017-05-30 09:41:47 +02:00 committed by Bence Romsics
parent 08089b01c4
commit da92f24bc2
16 changed files with 381 additions and 8 deletions

View File

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

View File

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

View File

@ -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<trunk_id>[^/]+)/$', AngularIndexView.as_view(title=title),
name='detail'),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
<div ng-controller="TrunkOverviewController as ctrl">
<hz-resource-property-list
resource-type-name="OS::Neutron::Trunk"
cls="dl-horizontal"
item="ctrl.trunk"
property-groups="[[
'id', 'name', 'description', 'port_id', 'status', 'admin_state',
'created_at', 'updated_at']]">
</hz-resource-property-list>
<h2 class="h4">{$ ::'Subports' | translate $}</h2>
<hr>
<dl class="dl-horizontal">
<hz-dynamic-table
config="ctrl.tableConfig"
items="ctrl.trunk.sub_ports"
table="ctrl">
</hz-dynamic-table>
</dl>
</div>

View File

@ -14,4 +14,4 @@
</div>
</dl>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
---
features:
- |
[`blueprint neutron-trunk-ui <https://blueprints.launchpad.net/horizon/+spec/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.