Add functionality to view portgroups

This is the first in a series of reviews that will add functionality
to manage portgroups within the Ironic UI. Specifically, this commit
adds the following items:

- API functions for listing, creating, and deleting portgroups
- A tabular view of the portgroups associated with a node in
  the node-details/configuration table
- The portgroup table utilizes expandable detail rows for
  giving the user access to additional information. Detail
  rows provide a scalable way to display additional information
  without resorting to a separate tab/page.
- The node-details/configuration ports table has been reworked
  to take advantage of detail rows.
- The batch and inividual delete-portgroup actions are working.

I am looking for feedback on all aspects of the new functionality,
including the use of detail rows in data tables.

Change-Id: I4c150db4e56fa6970cc112c87cdc54cb3fbb28e5
This commit is contained in:
Peter Piela 2017-06-14 15:09:59 -04:00
parent 7ce520e06b
commit 863e9e6295
12 changed files with 663 additions and 29 deletions

View File

@ -290,3 +290,40 @@ def port_update(request, port_uuid, patch):
port = ironicclient(request).port.update(port_uuid, patch)
return dict([(f, getattr(port, f, ''))
for f in res_fields.PORT_DETAILED_RESOURCE.fields])
def portgroup_list(request, node_id):
"""List the portgroups associated with a given node.
:param request: HTTP request.
:param node_id: The UUID or name of the node.
:return: A full list of portgroups. (limit=0)
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.list_portgroups
"""
return ironicclient(request).portgroup.list(node_id, limit=0, detail=True)
def portgroup_create(request, params):
"""Create a portgroup.
:param request: HTTP request.
:param params: Portgroup creation parameters.
:return: Portgroup.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.create
"""
portgroup_manager = ironicclient(request).portgroup
return portgroup_manager.create(**params)
def portgroup_delete(request, portgroup_id):
"""Delete a portgroup from the DB.
:param request: HTTP request.
:param portgroup_id: The UUID or name of the portgroup.
:return: Portgroup.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.delete
"""
return ironicclient(request).portgroup.delete(portgroup_id)

View File

@ -310,3 +310,40 @@ class DriverProperties(generic.View):
:return: Dictionary of properties
"""
return ironic.driver_properties(request, driver_name)
@urls.register
class Portgroups(generic.View):
url_regex = r'ironic/portgroups/$'
@rest_utils.ajax()
def get(self, request):
"""Get the list of portgroups associated with a specified node.
:param request: HTTP request.
:return: List of portgroups.
"""
portgroups = ironic.portgroup_list(request,
request.GET.get('node_id'))
return {
'portgroups': [i.to_dict() for i in portgroups]
}
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create a portgroup.
:param request: HTTP request.
:return: Portgroup.
"""
return ironic.portgroup_create(request, request.DATA).to_dict()
@rest_utils.ajax(data_required=True)
def delete(self, request):
"""Delete a portgroup.
:param request: HTTP request.
"""
return ironic.portgroup_delete(request,
request.DATA.get('portgroup_id'))

View File

@ -82,6 +82,22 @@
uuid: undefined
};
// Default portgroup object.
var defaultPortgroup = {
address: null,
created_at: null,
extra: {},
internal_info: {},
mode: "active-backup",
name: null,
node_uuid: undefined,
ports: [],
properties: {},
standalone_ports_supported: true,
updated_at: null,
uuid: undefined
};
// Value of the next available system port
var nextAvailableSystemPort = 1024;
@ -110,7 +126,8 @@
nodeGetConsoleUrl: nodeGetConsoleUrl,
getDrivers: getDrivers,
getImages: getImages,
getPort: getPort
getPort: getPort,
getPortgroup: getPortgroup
};
var responseCode = {
@ -127,6 +144,9 @@
// Dictionary of active ports indexed by port-uuid
var ports = {};
// Dictionary of active portgroups indexed by portgroup-uuid
var portgroups = {};
return service;
/**
@ -162,7 +182,8 @@
var backendNode = {
base: node,
consolePort: getNextAvailableSystemPort(),
ports: {} // Indexed by port-uuid
ports: {}, // Indexed by port-uuid
portgroups: {} // Indexed by portgroup-uuid
};
nodes[node.uuid] = backendNode;
@ -256,6 +277,47 @@
return [status, port];
}
/**
* @description Create a portgroup.
* This function is not yet fully implemented.
*
* @param {object} params - Dictionary of parameters that define
* the portgroup to be created.
* @return {object|null} Portgroup object, or null if the port could
* not be created.
*/
function createPortgroup(params) {
var portgroup = null;
var status = responseCode.BAD_QUERY;
if (angular.isDefined(nodes[params.node_uuid])) {
if (angular.isDefined(params.name) &&
params.name !== null &&
angular.isDefined(portgroups[params.name])) {
status = responseCode.RESOURCE_CONFLICT;
} else {
portgroup = angular.copy(defaultPortgroup);
angular.forEach(params, function(value, key) {
portgroup[key] = value;
});
if (angular.isUndefined(portgroup.uuid)) {
portgroup.uuid = uuidService.generate();
}
portgroups[portgroup.uuid] = portgroup;
if (portgroup.name !== null) {
portgroups[portgroup.name] = portgroup;
}
nodes[portgroup.node_uuid].portgroups[portgroup.uuid] = portgroup;
}
status = responseCode.SUCCESS;
}
return [status, portgroup];
}
/**
* description Get a specified port.
*
@ -267,6 +329,18 @@
return angular.isDefined(ports[portUuid]) ? ports[portUuid] : null;
}
/**
* description Get a specified portgroup.
*
* @param {string} portgroupId - Uuid or name of the requested portgroup.
* @return {object|null} Portgroup object, or null if the portgroup
* does not exist.
*/
function getPortgroup(portgroupId) {
return angular.isDefined(portgroups[portgroupId])
? portgroups[portgroupId] : null;
}
/**
* @description Initialize the Backend-Mock service.
* Create the handlers that intercept http requests.
@ -473,6 +547,45 @@
}
return [status, {ports: ports}];
});
// Create portgroup
$httpBackend.whenPOST(/\/api\/ironic\/portgroups\/$/)
.respond(function(method, url, data) {
return createPortgroup(JSON.parse(data));
});
// Get portgroups. This function is not fully implemented.
$httpBackend.whenGET(/\/api\/ironic\/ports\/$/)
.respond(function(method, url, data) {
var nodeId = JSON.parse(data).node_id;
var status = responseCode.RESOURCE_NOT_FOUND;
var portgroups = [];
if (angular.isDefined(nodes[nodeId])) {
angular.forEach(nodes[nodeId].portgroups, function(portgroup) {
portgroups.push(portgroup);
});
status = responseCode.SUCCESS;
}
return [status, {portgroups: portgroups}];
});
// Delete portgroup. This function is not yet implemented.
$httpBackend.whenDELETE(/\/api\/ironic\/portgroups\/$/)
.respond(function(method, url, data) {
var portgroupId = JSON.parse(data).portgroup_id;
var status = responseCode.RESOURCE_NOT_FOUND;
if (angular.isDefined(portgroups[portgroupId])) {
var portgroup = portgroups[portgroupId];
if (portgroup.name !== null) {
delete portgroups[portgroup.name];
delete portgroups[portgroup.uuid];
} else {
delete portgroups[portgroupId];
}
status = responseCode.EMPTY_RESPONSE;
}
return [status, ""];
});
}
/**

View File

@ -57,7 +57,10 @@
setNodeProvisionState: setNodeProvisionState,
updateNode: updateNode,
updatePort: updatePort,
validateNode: validateNode
validateNode: validateNode,
createPortgroup: createPortgroup,
getPortgroups: getPortgroups,
deletePortgroup: deletePortgroup
};
return service;
@ -513,6 +516,79 @@
return $q.reject(msg);
});
}
}
/**
* @description Retrieve a list of portgroups associated with a node.
*
* http://developer.openstack.org/api-ref/baremetal/#list-detailed-portgroups
*
* @param {string} nodeId UUID or logical name of a node.
* @return {promise} List of portgroups.
*/
function getPortgroups(nodeId) {
return apiService.get('/api/ironic/portgroups/',
{params: {node_id: nodeId}})
.then(function(response) {
// Add id property to support delete operations
// using the deleteModalService
angular.forEach(response.data.portgroups, function(portgroup) {
portgroup.id = portgroup.uuid;
});
return response.data.portgroups;
})
.catch(function(response) {
var msg = interpolate(
gettext('Unable to retrieve Ironic node portgroups: %s'),
[response.data],
false);
toastService.add('error', msg);
return $q.reject(msg);
});
}
/**
* @description Create a protgroup.
*
* http://developer.openstack.org/api-ref/baremetal/#create-portgroup
*
* @param {object} params Object containing parameters that define
* the portgroup to be created.
* @return {promise} Promise containing the portgroup.
*/
function createPortgroup(params) {
return apiService.post('/api/ironic/portgroups/', params)
.then(function(response) {
toastService.add('success',
gettext('Portgroup successfully created'));
return response.data; // The newly created portgroup
})
.catch(function(response) {
var msg = interpolate(gettext('Unable to create portgroup: %s'),
[response.data],
false);
toastService.add('error', msg);
return $q.reject(msg);
});
}
/**
* @description Delete a portgroup.
*
* http://developer.openstack.org/api-ref/baremetal/#delete-portgroup
*
* @param {string} portgroupId UUID or name of the portgroup to be deleted.
* @return {promise} Promise.
*/
function deletePortgroup(portgroupId) {
return apiService.delete('/api/ironic/portgroups/',
{portgroup_id: portgroupId})
.catch(function(response) {
var msg = interpolate(gettext('Unable to delete portgroup: %s'),
[response.data],
false);
toastService.add('error', msg);
return $q.reject(msg);
});
}
}
}());

View File

@ -20,12 +20,15 @@
var IRONIC_API_PROPERTIES = [
'createNode',
'createPort',
'createPortgroup',
'deleteNode',
'deletePort',
'deletePortgroup',
'getDrivers',
'getDriverProperties',
'getNode',
'getNodes',
'getPortgroups',
'getPortsWithNode',
'getBootDevice',
'nodeGetConsole',
@ -402,6 +405,112 @@
ironicBackendMockService.flush();
});
it('createPortgroup', function() {
var node;
createNode({driver: defaultDriver})
.then(function(createNode) {
node = createNode;
return ironicAPI.createPortgroup({node_uuid: node.uuid});
})
.then(function(portgroup) {
expect(portgroup.node_uuid).toBe(node.uuid);
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid));
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('createPortgroup - specify portgroup name', function() {
var node;
var portgroupName = "test-portgroup";
createNode({driver: defaultDriver})
.then(function(createNode) {
node = createNode;
return ironicAPI.createPortgroup({node_uuid: node.uuid,
name: portgroupName});
})
.then(function(portgroup) {
expect(portgroup.node_uuid).toBe(node.uuid);
expect(portgroup.name).toBe(portgroupName);
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid));
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.name));
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('createPortgroup - missing input data', function() {
ironicAPI.createPortgroup({})
.then(failTest);
ironicBackendMockService.flush();
});
it('createPort - bad input data', function() {
ironicAPI.createPort({node_uuid: ""})
.then(failTest);
ironicBackendMockService.flush();
});
it('deletePortgroup', function() {
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.createPortgroup({node_uuid: node.uuid});
})
.then(function(portgroup) {
expect(portgroup).toBeDefined();
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid));
ironicAPI.deletePortgroup(portgroup.uuid).then(function() {
expect(ironicBackendMockService.getPortgroup(portgroup.uuid))
.toBeNull();
});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('deletePortgroup - by name', function() {
var portgroupName = "delete-portgroup";
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.createPortgroup({node_uuid: node.uuid,
name: portgroupName});
})
.then(function(portgroup) {
expect(portgroup).toBeDefined();
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid));
expect(portgroup)
.toEqual(ironicBackendMockService.getPortgroup(portgroup.name));
ironicAPI.deletePortgroup(portgroup.name).then(function() {
expect(ironicBackendMockService.getPortgroup(portgroup.name))
.toBeNull();
expect(ironicBackendMockService.getPortgroup(portgroup.uuid))
.toBeNull();
});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('deletePortgroup - nonexistent portgroup', function() {
ironicAPI.deletePortgroup(0)
.then(failTest);
ironicBackendMockService.flush();
});
});
});
})();

View File

@ -59,6 +59,7 @@
var service = {
deleteNode: deleteNode,
deletePort: deletePort,
deletePortgroups: deletePortgroups,
setPowerState: setPowerState,
setMaintenance: setMaintenance,
setProvisionState: setProvisionState,
@ -188,7 +189,7 @@
'Successfully deleted ports "%s"',
ports.length),
error: ngettext('Unable to delete port "%s"',
'Unable to delete portss "%s"',
'Unable to delete ports "%s"',
ports.length)
},
deleteEntity: ironic.deletePort,
@ -197,6 +198,32 @@
return deleteModalService.open($rootScope, ports, context);
}
function deletePortgroups(portgroups) {
var context = {
labels: {
title: ngettext("Delete Portgroup",
"Delete Portgroups",
portgroups.length),
message: ngettext('Are you sure you want to delete portgroup "%s"? ' +
'This action cannot be undone.',
'Are you sure you want to delete portgroups "%s"? ' +
'This action cannot be undone.',
portgroups.length),
submit: ngettext("Delete Portgroup",
"Delete Portgroups",
portgroups.length),
success: ngettext('Successfully deleted portgroup "%s"',
'Successfully deleted portgroups "%s"',
portgroups.length),
error: ngettext('Unable to delete portgroup "%s"',
'Unable to delete portgroups "%s"',
portgroups.length)
},
deleteEntity: ironic.deletePortgroup
};
return deleteModalService.open($rootScope, portgroups, context);
}
/*
* @name horizon.dashboard.admin.ironic.actions.getPowerTransitions
* @description Get the list of power transitions for a specified

View File

@ -53,6 +53,7 @@
var path = basePath + '/node-details/sections/';
ctrl.noPortsText = gettext('No network ports have been defined');
ctrl.noPortgroupsText = gettext('No portgroups have been defined');
ctrl.actions = actions;
ctrl.maintenanceService = maintenanceService;
@ -68,6 +69,9 @@
}
];
ctrl.portDetailsTemplateUrl = path + "port-details.html";
ctrl.portgroupDetailsTemplateUrl = path + "portgroup-details.html";
ctrl.node = null;
ctrl.nodeValidation = [];
ctrl.nodeValidationMap = {}; // Indexed by interface
@ -75,6 +79,8 @@
ctrl.nodePowerTransitions = [];
ctrl.ports = [];
ctrl.portsSrc = [];
ctrl.portgroups = [];
ctrl.portgroupsSrc = [];
ctrl.basePath = basePath;
ctrl.re_uuid = new RegExp(validUuidPattern);
ctrl.isUuid = isUuid;
@ -85,6 +91,7 @@
ctrl.editPort = editPort;
ctrl.refresh = refresh;
ctrl.toggleConsoleMode = toggleConsoleMode;
ctrl.deletePortgroups = deletePortgroups;
$scope.emptyObject = function(obj) {
return angular.isUndefined(obj) || Object.keys(obj).length === 0;
@ -112,6 +119,7 @@
ctrl.nodePowerTransitions = actions.getPowerTransitions(ctrl.node);
retrievePorts();
retrieveBootDevice();
retrievePortgroups();
validateNode();
});
}
@ -161,6 +169,19 @@
});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePortgroups
* @description Retrieve the port groups associated with the current node,
* and store them in the controller instance.
*
* @return {void}
*/
function retrievePortgroups() {
ironic.getPortgroups(ctrl.node.uuid).then(function(portgroups) {
ctrl.portgroupsSrc = portgroups;
});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.validateNode
* @description Retrieve the ports associated with the current node,
@ -256,6 +277,19 @@
});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.portgroupDelete
* @description Delete a list of portgroups.
*
* @param {port []} portgroups portgroups to be deleted.
* @return {void}
*/
function deletePortgroups(portgroups) {
actions.deletePortgroups(portgroups).then(function() {
ctrl.refresh();
});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.refresh
* @description Update node information

View File

@ -69,6 +69,10 @@
return $q.when(ports);
},
getPortgroups: function() {
return $q.when([]);
},
getBootDevice: function () {
return $q.when(bootDevice);
},

View File

@ -1,7 +1,7 @@
<div class="row">
<!-- General -->
<div class="col-md-6 status detail">
<div class="col-md-12 status detail">
<h4 translate>General</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
@ -15,7 +15,9 @@
<dd>{$ ctrl.node.created_at | date:'medium' | noValue $}</dd>
</dl>
</div>
</div>
<div class="row">
<!-- Ports -->
<div class="col-md-6 status detail">
<h4 translate>Ports</h4>
@ -26,7 +28,7 @@
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th colspan="4" class="action-col">
<th colspan="100" class="action-col">
<action-list uib-dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
@ -50,11 +52,15 @@
<input type="checkbox"
hz-select-all="ctrl.ports"/>
</th>
<th>&nbsp;</th>
<th translate class="rsp-p1" style="white-space:nowrap">
MAC Address
</th>
<th translate class="rsp-p2" style="width:100%;">
Properties
<th translate class="rsp-p2" style="white-space:nowrap;">
PXE Enabled
</th>
<th translate class="rsp-p2" style="white-space:nowrap;">
Portgroup
</th>
<th translate class="actions_column">
Actions
@ -62,34 +68,24 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="port in ctrl.ports">
<tr ng-repeat-start="port in ctrl.ports">
<td class="multi_select_column">
<input type="checkbox"
hz-select="port"
ng-model="tCtrl.selections[port.id].checked"/>
<td>
<i class="fa fa-chevron-right"
hz-expand-detail="fa-chevron-right fa-chevron-down"
duration="200"
item="port"></i>
</td>
<td class="rsp-p1">
{$ port.address $}
</td>
<td>
<ul style="list-style:none;padding-left:0;">
<li><strong>pxe_enabled</strong>: {$ port.pxe_enabled $}</li>
<li ng-repeat="propertyObject in
['local_link_connection',
'extra']"
ng-if="!emptyObject(port[propertyObject])">
<strong>{$ propertyObject $}</strong>:
<ul style="list-style:none;padding-left:10px;">
<li ng-repeat="(id, value) in port[propertyObject]">
<strong>{$ id $}</strong>:
<span ng-switch="id">
<a ng-switch-when="vif_port_id"
href="/dashboard/admin/networks/ports/{$ value $}/detail">{$ value $}</a>
<span ng-switch-default>{$ value $}</span>
</span>
</li>
</ul>
</li>
</ul>
{$ port.pxe_enabled $}
<td>
{$ port.portgroup_uuid | noValue $}
</td>
<td class="actions_column">
<action-list uib-dropdown class="pull-right">
@ -113,6 +109,13 @@
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="100">
<hz-detail-row
template-url="ctrl.portDetailsTemplateUrl">
</hz-detail-row>
</td>
</tr>
<tr hz-no-items
items="ctrl.ports"
message="ctrl.noPortsText">
@ -120,6 +123,113 @@
</tbody>
</table>
</div>
<!-- Portgroups -->
<div class="col-md-6 status detail">
<h4 translate>Portgroups</h4>
<hr class="header_rule">
<table hz-table ng-cloak
st-table="ctrl.portgroups"
st-safe-src="ctrl.portgroupsSrc"
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th colspan="100" class="action-col">
<action-list uib-dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="">
{$ ::'Create portgroup' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.deletePortgroups"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
<span class="fa fa-trash"></span>
{$ ::'Delete portgroups' | translate $}
</action>
</menu>
</action-list>
</th>
</tr>
<tr>
<th class="multi_select_column">
<input type="checkbox"
hz-select-all="ctrl.portgroups"/>
</th>
<th>&nbsp;</th>
<th translate class="rsp-p1" style="width:100%;">
UUID
</th>
<th translate class="rsp-p2" style="white-space:nowrap;">
MAC Address
</th>
<th translate class="rsp-p2" style="width:white-space:nowrap;">
Name
</th>
<th translate class="actions_column">
Actions
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat-start="portgroup in ctrl.portgroups">
<td class="multi_select_column">
<input type="checkbox"
hz-select="portgroup"
ng-model="tCtrl.selections[portgroup.id].checked"/>
<td>
<i class="fa fa-chevron-right"
hz-expand-detail="fa-chevron-right fa-chevron-down"
duration="200"
item="portgroup"></i>
</td>
<td class="rsp-p1">
{$ portgroup.uuid $}
</td>
<td class="rsp-p1">
{$ portgroup.address | noValue $}
</td>
<td class="rsp-p1">
{$ portgroup.name | noValue $}
</td>
<td class="actions_column">
<action-list uib-dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback=""
item="port">
{$ ::'Edit portgroup' | translate $}
</action>
<menu>
<li role="presentation">
<a role="menuitem"
ng-click="ctrl.deletePortgroups([portgroup]);
$event.stopPropagation();
$event.preventDefault()">
<span class="fa fa-trash"></span>
<span>{$ :: 'Delete portgroup' | translate $}</span>
</a>
</li>
</menu>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="100">
<hz-detail-row
template-url="ctrl.portgroupDetailsTemplateUrl">
</hz-detail-row>
</td>
</tr>
<tr hz-no-items
items="ctrl.portgroups"
message="ctrl.noPortgroupsText">
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">

View File

@ -0,0 +1,32 @@
<div class="row">
<div class="col-md-4">
<h6 class="text-center" translate>Attributes</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt>UUID</dt>
<dd>{$ port.uuid $}
</dl>
</div>
<div class="col-md-4">
<h6 class="text-center" translate>Local Link Connection</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt ng-repeat-start="(id, value) in port.local_link_connection">{$ id $}</dt>
<dd ng-repeat-end>{$ value $}</dd>
</dl>
</div>
<div class="col-md-4">
<h6 class="text-center" translate>Extra</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt ng-repeat-start="(id, value) in port.extra">{$ id $}</dt>
<dd ng-repeat-end>
<span ng-switch="id">
<a ng-switch-when="vif_port_id"
href="/dashboard/admin/networks/ports/{$ value $}/detail">{$ value $}</a>
<span ng-switch-default>{$ value $}</span>
</span>
</dd>
</dl>
</div>
</div>

View File

@ -0,0 +1,28 @@
<div class="row">
<div class="col-md-4">
<h6 class="text-center" translate>Attributes</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt>Mode</dt>
<dd>{$ portgroup.mode $}</dd>
<dt><abbr title="Standalone">SA</abbr> ports</dt>
<dd>{$ portgroup.standalone_ports_supported $}</dd>
</dl>
</div>
<div class="col-md-4">
<h6 class="text-center" translate>Properties</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt ng-repeat-start="(id, value) in portgroup.properties">{$ id $}</dt>
<dd ng-repeat-end>{$ value $}</dd>
</dl>
</div>
<div class="col-md-4">
<h6 class="text-center" translate>Extra</h6>
<hr style="margin:0;">
<dl class="dl-horizontal">
<dt ng-repeat-start="(id, value) in portgroup.extra">{$ id $}</dt>
<dd ng-repeat-end>{$ value $}</dd>
</dl>
</div>
</div>

View File

@ -0,0 +1,27 @@
---
features:
- |
Support has been added for viewing and managing the portgroups
associated with an Ironic node.
- |
A portgroup table has been added to the node-details/configuration tab.
- |
Each row in the table displays a single portgroup, and has column entries
for its UUID, MAC address, name, and number of ports. A dropdown menu
is also provided that contains actions that can be applied to the
portgroup.
- |
Detailed information for a portgroup is obtained by clicking the
detail-toggle-selector (right-chevron) located in its table row.
The additional information is displayed in a row expansion.
- |
The port table in node-details/configuration tab has been modified
as follows:
* A column has been added that displays the UUID of the portgroup
to which the port belongs.
* The ``Properties`` column has been replaced with a column that
displays only the pxe_enabled property.
* Additional properties are displayed by clicking the
detail-toggle-selector for that port in a similar manner to the
portgroup table.