Trunks panel: eliminate spinner at create/edit

Change how the UI reacts to neutron responding to API requests.

Without this change when the user clicks create or edit:
* we display a spinner
* wait for the neutron to respond
* then we open the modal.

With this change:
* we open the modal immediately and
* send the request(s) to neutron asynchronously.
* We display a 'please wait' message in place of the relevant (but not
  all) input forms (or transfer tables) of the workflow steps.
* When neutron responds to each request we replace the 'please wait'
  message with the pre-filled input forms.

The latter is better experience for the user because he or she can
progress with parts of the workflow until the rest is loaded.

Partially-Implements: blueprint neutron-trunk-ui
Change-Id: I9ac8f75a390424ad05cf51fa679ef9803124179c
This commit is contained in:
Bence Romsics 2017-11-27 16:05:50 +01:00
parent 0a51ce628d
commit 705c52bf1f
13 changed files with 780 additions and 727 deletions

View File

@ -30,7 +30,6 @@
'horizon.app.core.trunks.actions.ports-extra.service',
'horizon.app.core.trunks.resourceType',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.modal-wait-spinner.service',
// Using horizon.framework.widgets.form.ModalFormService and
// 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.
@ -52,7 +51,6 @@
portsExtra,
resourceType,
actionResultService,
spinnerService,
wizardModalService,
toast
) {
@ -74,65 +72,36 @@
}
function perform() {
// NOTE(bence romsics): Suboptimal UX. We delay opening the modal until
// neutron objects are loaded. But ideally the steps independent of
// already existing neutron objects (ie. the trunk details step) could
// already work while loading neutron stuff in the background.
spinnerService.showModalSpinner(gettext('Please Wait'));
return $q.all({
// TODO(bence romsics): Query filters of port and network listings
// should be aligned. While here it looks like we query all
// possible ports and all possible networks this is not really the
// case. A few calls down in openstack_dashboard/api/neutron.py
// we have a filterless port listing, but networks are listed
// by network_list_for_tenant() which includes some hardcoded
// tenant-specific filtering. Therefore here we may get back some
// ports whose networks we don't have. This is only a problem for
// admin and even then it means just missing network and subnet
// metadata on some ports. But anyway when we want this panel to
// work for admin too, we should fix this.
getNetworks: neutron.getNetworks(),
getPorts: userSession.get().then(function(session) {
return neutron.getPorts({project_id: session.project_id});
})
}).then(function(responses) {
var networks = responses.getNetworks.data.items;
var ports = responses.getPorts.data.items;
return {
parentPortCandidates: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isParentPortCandidate),
networks),
subportCandidates: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isSubportCandidate),
networks)
};
}).then(openModal);
function openModal(params) {
spinnerService.hideModalSpinner();
return wizardModalService.modal({
workflow: createWorkflow,
submit: submit,
data: {
initTrunk: {
admin_state_up: true,
description: '',
name: '',
port_id: undefined,
sub_ports: []
},
ports: {
parentPortCandidates: params.parentPortCandidates.sort(
portsExtra.cmpPortsByNameAndId),
subportCandidates: params.subportCandidates.sort(
portsExtra.cmpPortsByNameAndId),
subportsOfInitTrunk: []
}
}
}).result;
}
// NOTE(bence romsics): The parent and subport selector steps are shared
// by the create and edit workflows, therefore we have to initialize the
// trunk adequately in both cases. That is an empty trunk for create and
// the trunk to be updated for edit.
var trunk = {
admin_state_up: true,
description: '',
name: '',
port_id: undefined,
sub_ports: []
};
return wizardModalService.modal({
workflow: createWorkflow,
submit: submit,
data: {
initTrunk: trunk,
getTrunk: $q.when(trunk),
getPortsWithNets: $q.all({
getNetworks: neutron.getNetworks(),
getPorts: userSession.get().then(function(session) {
return neutron.getPorts({project_id: session.project_id});
})
}).then(function(responses) {
var networks = responses.getNetworks.data.items;
var ports = responses.getPorts.data.items;
return portsExtra.addNetworkAndSubnetInfo(
ports, networks).sort(portsExtra.cmpPortsByNameAndId);
})
}
}).result;
}
function submit(stepModels) {

View File

@ -104,22 +104,17 @@
it('open the modal with the correct parameters', function() {
spyOn(wizardModalService, 'modal').and.callThrough();
spyOn(modalWaitSpinnerService, 'showModalSpinner');
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
service.perform();
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Please Wait');
$timeout.flush();
expect(wizardModalService.modal).toHaveBeenCalled();
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
expect(modalArgs.scope).toBeUndefined();
expect(modalArgs.workflow).toBeDefined();
expect(modalArgs.submit).toBeDefined();
expect(modalArgs.data.initTrunk).toBeDefined();
expect(modalArgs.data.ports).toBeDefined();
expect(modalArgs.data.getTrunk).toBeDefined();
expect(modalArgs.data.getPortsWithNets).toBeDefined();
});
it('should submit create trunk request to neutron', function() {

View File

@ -23,6 +23,7 @@
editService.$inject = [
'$q',
'$location',
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.userSession',
@ -30,7 +31,6 @@
'horizon.app.core.trunks.actions.ports-extra.service',
'horizon.app.core.trunks.resourceType',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.modal-wait-spinner.service',
'horizon.framework.widgets.modal.wizard-modal.service',
'horizon.framework.widgets.toast.service'
];
@ -42,6 +42,7 @@
*/
function editService(
$q,
$location,
neutron,
policy,
userSession,
@ -49,7 +50,6 @@
portsExtra,
resourceType,
actionResultService,
spinnerService,
wizardModalService,
toast
) {
@ -71,48 +71,37 @@
}
function perform(selected) {
// See also at perform() in create action.
spinnerService.showModalSpinner(gettext('Please Wait'));
var params = {};
return $q.all({
getNetworks: neutron.getNetworks(),
getPorts: userSession.get().then(function(session) {
return neutron.getPorts({project_id: session.project_id});
}),
getTrunk: neutron.getTrunk(selected.id)
}).then(function(responses) {
var networks = responses.getNetworks.data.items;
var ports = responses.getPorts.data.items;
var trunk = responses.getTrunk.data;
return {
subportCandidates: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isSubportCandidate),
networks),
subportsOfInitTrunk: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isSubportOfTrunk.bind(null, selected.id)),
networks),
trunk: trunk
};
}).then(openModal);
function openModal(params) {
spinnerService.hideModalSpinner();
return wizardModalService.modal({
workflow: editWorkflow,
submit: submit,
data: {
initTrunk: params.trunk,
ports: {
parentPortCandidates: [],
subportCandidates: params.subportCandidates.sort(
portsExtra.cmpPortsByNameAndId),
subportsOfInitTrunk: params.subportsOfInitTrunk.sort(
portsExtra.cmpSubportsBySegmentationTypeAndId)
}
}
}).result;
if ($location.url().indexOf('admin') === -1) {
params = {project_id: userSession.project_id};
}
return wizardModalService.modal({
workflow: editWorkflow,
submit: submit,
data: {
// The step controllers can and will freshly query the trunk
// by using the getTrunk promise below. For all updateable
// attributes you should use that. But to make our lives a bit
// easier we also pass synchronously (and redundantly) the trunk
// we queried earlier. Remember to only use those attributes
// of it that are not allowed to be updated.
initTrunk: selected,
getTrunk: neutron.getTrunk(selected.id).then(function(response) {
return response.data;
}),
getPortsWithNets: $q.all({
getNetworks: neutron.getNetworks(params),
getPorts: neutron.getPorts(params)
}).then(function(responses) {
var networks = responses.getNetworks.data.items;
var ports = responses.getPorts.data.items;
return portsExtra.addNetworkAndSubnetInfo(
ports, networks).sort(portsExtra.cmpPortsByNameAndId);
})
}
}).result;
}
function submit(stepModels) {

View File

@ -109,22 +109,17 @@
it('open the modal with the correct parameters', function() {
spyOn(wizardModalService, 'modal').and.callThrough();
spyOn(modalWaitSpinnerService, 'showModalSpinner');
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
service.perform({id: 1});
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Please Wait');
$timeout.flush();
expect(wizardModalService.modal).toHaveBeenCalled();
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
expect(modalArgs.scope).toBeUndefined();
expect(modalArgs.workflow).toBeDefined();
expect(modalArgs.submit).toBeDefined();
expect(modalArgs.data.initTrunk).toBeDefined();
expect(modalArgs.data.ports).toBeDefined();
expect(modalArgs.data.getTrunk).toBeDefined();
expect(modalArgs.data.getPortsWithNets).toBeDefined();
});
it('should submit edit trunk request to neutron', function() {

View File

@ -43,46 +43,50 @@
{ label: gettext('Disabled'), value: false }
];
ctrl.trunk = {
admin_state_up: $scope.initTrunk.admin_state_up,
description: $scope.initTrunk.description,
name: $scope.initTrunk.name
};
$scope.getTrunk.then(function(trunk) {
ctrl.trunk = {
admin_state_up: trunk.admin_state_up,
description: trunk.description,
name: trunk.name
};
// NOTE(bence romsics): The step controllers are naturally stateful,
// but the actions should be stateless. However we have to
// get back the captured user input from the step controller to the
// action, because the action makes the neutron call. WizardController
// helps us and passes $scope.stepModels to the actions' submit().
// Also note that $scope.stepModels is shared between all workflow
// steps.
//
// We roughly follow the example discussed and presented here:
// http://lists.openstack.org/pipermail/openstack-dev/2016-July/099368.html
// https://review.openstack.org/345145
//
// Though we deviate a bit in the use of stepModels. The example
// has one model object per step, named after the workflow step's
// form. Instead we treat stepModels as a generic state variable. See
// the details below.
//
// The trunkSlices closures return a slice of the trunk model which
// can be then merged by the action to get the whole trunk model. By
// using closures we can spare the use of watchers and the constant
// recomputation of the trunk slices even in the more complicated
// other steps.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getDetails = function() {
return ctrl.trunk;
};
// NOTE(bence romsics): The step controllers are naturally stateful,
// but the actions should be stateless. However we have to
// get back the captured user input from the step controller to the
// action, because the action makes the neutron call. WizardController
// helps us and passes $scope.stepModels to the actions' submit().
// Also note that $scope.stepModels is shared between all workflow
// steps.
//
// We roughly follow the example discussed and presented here:
// http://lists.openstack.org/pipermail/openstack-dev/2016-July/099368.html
// https://review.openstack.org/345145
//
// Though we deviate a bit in the use of stepModels. The example
// has one model object per step, named after the workflow step's
// form. Instead we treat stepModels as a generic state variable. See
// the details below.
//
// The trunkSlices closures return a slice of the trunk model which
// can be then merged by the action to get the whole trunk model. By
// using closures we can spare the use of watchers and the constant
// recomputation of the trunk slices even in the more complicated
// other steps.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getDetails = function() {
return ctrl.trunk;
};
// In order to keep the update action stateless, we pass the old
// state of the trunk down to the step controllers, then back up
// to the update action's submit(). An alternative would be to
// eliminate the need for the old state of the trunk at update,
// at the price of moving the trunk diffing logic from python to
// javascript (ie. the subports step controller).
$scope.stepModels.initTrunk = $scope.initTrunk;
// In order to keep the update action stateless, we pass the old
// state of the trunk down to the step controllers, then back up
// to the update action's submit(). An alternative would be to
// eliminate the need for the old state of the trunk at update,
// at the price of moving the trunk diffing logic from python to
// javascript (ie. the subports step controller).
$scope.stepModels.initTrunk = $scope.initTrunk;
ctrl.trunkLoaded = true;
});
}

View File

@ -21,17 +21,20 @@
beforeEach(module('horizon.app.core.trunks'));
describe('TrunkDetailsController', function() {
var scope, ctrl;
var $q, $timeout, scope, ctrl;
//beforeEach(module('horizon.app.core.trunks.actions'));
beforeEach(inject(function($rootScope, $injector, $controller) {
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
$q = _$q_;
$timeout = _$timeout_;
scope = $rootScope.$new();
scope.stepModels = {};
scope.initTrunk = {
var trunk = {
admin_state_up: true,
description: '',
name: 'trunk1'
};
scope.initTrunk = trunk;
scope.getTrunk = $q.when(trunk);
ctrl = $controller('TrunkDetailsController', {
$scope: scope
@ -45,17 +48,23 @@
});
it('has trunk property', function() {
expect(ctrl.trunk).toBeDefined();
expect(ctrl.trunk.admin_state_up).toBeDefined();
expect(ctrl.trunk.admin_state_up).toEqual(true);
expect(ctrl.trunk.description).toBeDefined();
expect(ctrl.trunk.name).toBeDefined();
scope.getTrunk.then(function() {
expect(ctrl.trunk).toBeDefined();
expect(ctrl.trunk.admin_state_up).toBeDefined();
expect(ctrl.trunk.admin_state_up).toEqual(true);
expect(ctrl.trunk.description).toBeDefined();
expect(ctrl.trunk.name).toBeDefined();
});
$timeout.flush();
});
it('should return with trunk', function() {
var trunk = scope.stepModels.trunkSlices.getDetails();
expect(trunk.name).toEqual('trunk1');
expect(trunk.admin_state_up).toBe(true);
scope.getTrunk.then(function() {
var trunk = scope.stepModels.trunkSlices.getDetails();
expect(trunk.name).toEqual('trunk1');
expect(trunk.admin_state_up).toBe(true);
});
$timeout.flush();
});
});

View File

@ -7,44 +7,51 @@
</p>
<div class="content">
<div ng-if="!ctrl.trunkLoaded">
<span translate class="subtitle text-info">
Loading trunk... Please Wait
</span>
</div>
<div class="selected-source">
<div class="row form-group">
<div class="col-xs-8 col-sm-8">
<div class="form-group">
<label class="control-label" for="trunkForm-name">
<translate>Name</translate>
</label>
<input id="trunkForm-name" name="name"
type="text" class="form-control"
ng-model="ctrl.trunk.name">
<div ng-if="ctrl.trunkLoaded">
<div class="selected-source">
<div class="row form-group">
<div class="col-xs-8 col-sm-8">
<div class="form-group">
<label class="control-label" for="trunkForm-name">
<translate>Name</translate>
</label>
<input id="trunkForm-name" name="name"
type="text" class="form-control"
ng-model="ctrl.trunk.name">
</div>
</div>
</div>
<div class="col-xs-4 col-sm-4">
<div class="form-group">
<label class="control-label">
<translate>Admin State</translate>
</label>
<div class="form-field">
<div class="btn-group">
<label class="btn btn-default"
ng-repeat="option in ctrl.trunkAdminStateOptions"
ng-model="ctrl.trunk.admin_state_up"
uib-btn-radio="option.value">{$ ::option.label $}</label>
<div class="col-xs-4 col-sm-4">
<div class="form-group">
<label class="control-label">
<translate>Admin State</translate>
</label>
<div class="form-field">
<div class="btn-group">
<label class="btn btn-default"
ng-repeat="option in ctrl.trunkAdminStateOptions"
ng-model="ctrl.trunk.admin_state_up"
uib-btn-radio="option.value">{$ ::option.label $}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row form-group">
<div class="col-xs-12 col-sm-12">
<div class="form-group">
<label class="control-label" for="trunkForm-description">
<translate>Description</translate>
</label>
<input id="trunkForm-description" name="description"
type="text" class="form-control"
ng-model="ctrl.trunk.description">
<div class="row form-group">
<div class="col-xs-12 col-sm-12">
<div class="form-group">
<label class="control-label" for="trunkForm-description">
<translate>Description</translate>
</label>
<input id="trunkForm-description" name="description"
type="text" class="form-control"
ng-model="ctrl.trunk.description">
</div>
</div>
</div>
</div>

View File

@ -32,6 +32,7 @@
TrunkParentPortController.$inject = [
'$scope',
'horizon.app.core.trunks.actions.ports-extra.service',
'horizon.app.core.trunks.portConstants',
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
'horizon.framework.widgets.transfer-table.events'
@ -39,11 +40,13 @@
function TrunkParentPortController(
$scope,
portsExtra,
portConstants,
tooltipService,
ttevents
) {
var ctrl = this;
var parentPortCandidates;
ctrl.portStatuses = portConstants.statuses;
ctrl.portAdminStates = portConstants.adminStates;
@ -63,61 +66,72 @@
maxAllocation: 1
};
ctrl.parentTables = {
available: $scope.ports.parentPortCandidates,
allocated: [],
displayedAvailable: [],
displayedAllocated: []
};
$scope.getPortsWithNets.then(function(portsWithNets) {
parentPortCandidates = portsWithNets.filter(
portsExtra.isParentPortCandidate);
// See also in the details step controller.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getParentPort = function() {
var trunk = {port_id: $scope.initTrunk.port_id};
ctrl.parentTables = {
available: parentPortCandidates,
allocated: [],
displayedAvailable: [],
displayedAllocated: []
};
if (ctrl.parentTables.allocated.length in [0, 1]) {
trunk.port_id = ctrl.parentTables.allocated[0].id;
} else {
// maxAllocation is 1, so this should never happen.
throw new Error('Allocating multiple parent ports is meaningless.');
// See also in the details step controller.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getParentPort = function() {
var trunk = {port_id: $scope.initTrunk.port_id};
if (ctrl.parentTables.allocated.length in [0, 1]) {
trunk.port_id = ctrl.parentTables.allocated[0].id;
} else {
// maxAllocation is 1, so this should never happen.
throw new Error('Allocating multiple parent ports is meaningless.');
}
return trunk;
};
// We expose the allocated table directly to the subports step
// controller, so it can set watchers on it and react accordingly...
$scope.stepModels.allocated = $scope.stepModels.allocated || {};
$scope.stepModels.allocated.parentPort = ctrl.parentTables.allocated;
// ...and vice versa.
var deregisterAllocatedWatcher = $scope.$watchCollection(
'stepModels.allocated.subports', hideAllocated);
$scope.$on('$destroy', function() {
deregisterAllocatedWatcher();
});
function hideAllocated(allocatedList) {
if (!ctrl.portsLoaded || !allocatedList) {
return;
}
var allocatedDict = {};
var availableList;
allocatedList.forEach(function(port) {
allocatedDict[port.id] = true;
});
availableList = parentPortCandidates.filter(
function(port) {
return !(port.id in allocatedDict);
}
);
ctrl.parentTables.available = availableList;
// Notify transfertable.
$scope.$broadcast(
ttevents.TABLES_CHANGED,
{data: {available: availableList}}
);
}
return trunk;
};
// We expose the allocated table directly to the subports step
// controller, so it can set watchers on it and react accordingly...
$scope.stepModels.allocated = $scope.stepModels.allocated || {};
$scope.stepModels.allocated.parentPort = ctrl.parentTables.allocated;
// ...and vice versa.
var deregisterAllocatedWatcher = $scope.$watchCollection(
'stepModels.allocated.subports', hideAllocated);
$scope.$on('$destroy', function() {
deregisterAllocatedWatcher();
ctrl.portsLoaded = true;
});
function hideAllocated(allocatedList) {
var allocatedDict = {};
var availableList;
allocatedList.forEach(function(port) {
allocatedDict[port.id] = true;
});
availableList = $scope.ports.parentPortCandidates.filter(
function(port) {
return !(port.id in allocatedDict);
}
);
ctrl.parentTables.available = availableList;
// Notify transfertable.
$scope.$broadcast(
ttevents.TABLES_CHANGED,
{data: {available: availableList}}
);
}
}
})();

View File

@ -23,22 +23,23 @@
beforeEach(module('horizon.app.core.trunks'));
describe('TrunkParentPortController', function() {
var scope, ctrl, ttevents;
var $q, $timeout, $scope, ctrl;
beforeEach(inject(function($rootScope, $controller, $injector) {
scope = $rootScope.$new();
scope.ports = {
parentPortCandidates: [{id: 1}, {id: 2}]
};
scope.stepModels = {};
scope.initTrunk = {
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
$q = _$q_;
$timeout = _$timeout_;
$scope = $rootScope.$new();
$scope.getPortsWithNets = $q.when([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
$scope.stepModels = {};
$scope.initTrunk = {
port_id: 1
};
ttevents = $injector.get('horizon.framework.widgets.transfer-table.events');
ctrl = $controller('TrunkParentPortController', {
$scope: scope
$scope: $scope
});
}));
@ -78,38 +79,54 @@
});
it('uses scope to set table data', function() {
expect(ctrl.parentTables).toBeDefined();
expect(ctrl.parentTables.available).toEqual(
[{id: 1}, {id: 2}]);
expect(ctrl.parentTables.allocated).toEqual([]);
expect(ctrl.parentTables.displayedAllocated).toEqual([]);
expect(ctrl.parentTables.displayedAvailable).toEqual([]);
$scope.getPortsWithNets.then(function() {
expect(ctrl.parentTables).toBeDefined();
expect(ctrl.parentTables.available).toEqual([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
expect(ctrl.parentTables.allocated).toEqual([]);
expect(ctrl.parentTables.displayedAllocated).toEqual([]);
expect(ctrl.parentTables.displayedAvailable).toEqual([]);
});
$timeout.flush();
});
it('should return with parent port', function() {
ctrl.parentTables.allocated = [{id: 3}];
var trunk = scope.stepModels.trunkSlices.getParentPort();
expect(trunk.port_id).toEqual(3);
$scope.getPortsWithNets.then(function() {
ctrl.parentTables.allocated = [{id: 3}];
var trunk = $scope.stepModels.trunkSlices.getParentPort();
expect(trunk.port_id).toEqual(3);
});
$timeout.flush();
});
it('should throw exception if more than on port is allocated', function() {
ctrl.parentTables.allocated = [{id: 3}, {id: 4}];
expect(scope.stepModels.trunkSlices.getParentPort).toThrow();
it('should throw exception if more than one port is allocated', function() {
$scope.getPortsWithNets.then(function() {
ctrl.parentTables.allocated = [{id: 3}, {id: 4}];
expect($scope.stepModels.trunkSlices.getParentPort).toThrow();
});
$timeout.flush();
});
it('should remove port from available list if subportstable changes', function() {
spyOn(scope, '$broadcast').and.callThrough();
$scope.getPortsWithNets = $q.when([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''},
{id: 3, admin_state_up: true, device_owner: ''}
]);
$scope.stepModels.allocated = {};
$scope.stepModels.allocated.subports = [{id: 3}];
ctrl.parentTables.available = [{id: 1}, {id: 2}, {id: 3}];
scope.stepModels.allocated.subports = [{id: 3}];
$scope.getPortsWithNets.then(function() {
ctrl.portsLoaded = true;
scope.$digest();
expect(scope.$broadcast).toHaveBeenCalledWith(
ttevents.TABLES_CHANGED,
{data: {available: [{id: 1}, {id: 2}]}}
);
expect(ctrl.parentTables.available).toEqual([{id: 1}, {id: 2}]);
expect(ctrl.parentTables.available).toEqual([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
});
$scope.$digest();
});
});

View File

@ -7,159 +7,165 @@
be created. Mandatory.
</p>
<transfer-table tr-model="ctrl.parentTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated ng-model="ctrl.parentTables.allocated.length" validate-number-min="1">
<table st-table="ctrl.parentTables.displayedAllocated" st-safe-src="ctrl.parentTables.allocated"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.parentTables.allocated.length === 0">
<td colspan="7">
<div class="no-rows-help" translate>
Select an item from Available items below
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.parentTables.displayedAllocated track by item.id"
lr-drag-data="ctrl.parentTables.displayedAllocated"
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate" item="item">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="7" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>Device Owner</dt>
<dd>{$ item.device_owner $}</dd>
<dt translate>Device ID</dt>
<dd>{$ item.device_id $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</allocated>
<div ng-if="!ctrl.portsLoaded">
<span translate class="subtitle text-info">
Loading ports... Please Wait
</span>
</div>
<available>
<table st-table="ctrl.parentTables.displayedAvailable" st-safe-src="ctrl.parentTables.available"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="search-header" colspan="6">
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
</th>
</tr>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="6">
<div class="no-rows-help" translate>
No available items
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.parentTables.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate" item="item">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="6" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>Device Owner</dt>
<dd>{$ item.device_owner $}</dd>
<dt translate>Device ID</dt>
<dd>{$ item.device_id $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
<div ng-if="ctrl.portsLoaded">
<transfer-table tr-model="ctrl.parentTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated ng-model="ctrl.parentTables.allocated.length" validate-number-min="1">
<table st-table="ctrl.parentTables.displayedAllocated" st-safe-src="ctrl.parentTables.allocated"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.parentTables.allocated.length === 0">
<td colspan="7">
<div class="no-rows-help" translate>
Select an item from Available items below
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.parentTables.displayedAllocated track by item.id"
lr-drag-data="ctrl.parentTables.displayedAllocated"
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate" item="item">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="7" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>Device Owner</dt>
<dd>{$ item.device_owner $}</dd>
<dt translate>Device ID</dt>
<dd>{$ item.device_id $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<table st-table="ctrl.parentTables.displayedAvailable" st-safe-src="ctrl.parentTables.available"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="search-header" colspan="6">
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
</th>
</tr>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="6">
<div class="no-rows-help" translate>
No available items
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.parentTables.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate" item="item">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="6" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>Device ID</dt>
<dd>{$ item.device_id $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table>
</div>
</div>

View File

@ -31,6 +31,7 @@
TrunkSubPortsController.$inject = [
'$scope',
'horizon.app.core.trunks.actions.ports-extra.service',
'horizon.app.core.trunks.portConstants',
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
'horizon.framework.widgets.transfer-table.events'
@ -38,11 +39,13 @@
function TrunkSubPortsController(
$scope,
portsExtra,
portConstants,
tooltipService,
ttevents
) {
var ctrl = this;
var subportCandidates;
ctrl.portStatuses = portConstants.statuses;
ctrl.portAdminStates = portConstants.adminStates;
@ -71,80 +74,92 @@
ctrl.segmentationTypes = Object.keys(ctrl.segmentationTypesDict);
ctrl.subportsDetails = {};
$scope.initTrunk.sub_ports.forEach(function(subport) {
ctrl.subportsDetails[subport.port_id] = {
segmentation_type: subport.segmentation_type,
segmentation_id: subport.segmentation_id
$scope.getTrunk.then(function(trunk) {
trunk.sub_ports.forEach(function(subport) {
ctrl.subportsDetails[subport.port_id] = {
segmentation_type: subport.segmentation_type,
segmentation_id: subport.segmentation_id
};
});
ctrl.trunkLoaded = true;
});
$scope.getPortsWithNets.then(function(portsWithNets) {
var subportsOfInitTrunk = portsWithNets.filter(
portsExtra.isSubportOfTrunk.bind(null, $scope.initTrunk.id));
subportCandidates = portsWithNets.filter(
portsExtra.isSubportCandidate);
ctrl.subportsTables = {
available: [].concat(subportsOfInitTrunk, subportCandidates),
allocated: [].concat(subportsOfInitTrunk),
displayedAvailable: [],
displayedAllocated: []
};
});
ctrl.subportsTables = {
available: [].concat(
$scope.ports.subportsOfInitTrunk,
$scope.ports.subportCandidates),
// NOTE(bence romsics): Trunk information merged into ports and trunk
// information in initTrunk may occasionally be out of sync. Theoratically
// there's a chance to get rid of this, but that refactor will go deep.
allocated: $scope.ports.subportsOfInitTrunk,
displayedAvailable: [],
displayedAllocated: []
};
// See also in the details step controller.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getSubports = function() {
var trunk = {sub_ports: []};
// See also in the details step controller.
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
$scope.stepModels.trunkSlices.getSubports = function() {
var trunk = {sub_ports: []};
ctrl.subportsTables.allocated.forEach(function(port) {
// Subport information comes from two sources, one handled by
// transfertable, the other from outside of transfertable. We
// may see the two data structures in an inconsistent state. We
// skip the inconsistent cases by the following condition.
if (port.id in ctrl.subportsDetails) {
trunk.sub_ports.push({
port_id: port.id,
segmentation_id: ctrl.subportsDetails[port.id].segmentation_id,
segmentation_type: ctrl.subportsDetails[port.id].segmentation_type
});
}
});
ctrl.subportsTables.allocated.forEach(function(port) {
// Subport information comes from two sources, one handled by
// transfertable, the other from outside of transfertable. We
// may see the two data structures in an inconsistent state. We
// skip the inconsistent cases by the following condition.
if (port.id in ctrl.subportsDetails) {
trunk.sub_ports.push({
port_id: port.id,
segmentation_id: ctrl.subportsDetails[port.id].segmentation_id,
segmentation_type: ctrl.subportsDetails[port.id].segmentation_type
});
}
return trunk;
};
// We expose the allocated table directly to the parent port step
// controller, so it can set watchers on it and react accordingly...
$scope.stepModels.allocated = $scope.stepModels.allocated || {};
$scope.stepModels.allocated.subports = ctrl.subportsTables.allocated;
// ...and vice versa.
var deregisterAllocatedWatcher = $scope.$watchCollection(
'stepModels.allocated.parentPort', hideAllocated);
$scope.$on('$destroy', function() {
deregisterAllocatedWatcher();
});
return trunk;
};
// We expose the allocated table directly to the parent port step
// controller, so it can set watchers on it and react accordingly...
$scope.stepModels.allocated = $scope.stepModels.allocated || {};
$scope.stepModels.allocated.subports = ctrl.subportsTables.allocated;
// ...and vice versa.
var deregisterAllocatedWatcher = $scope.$watchCollection(
'stepModels.allocated.parentPort', hideAllocated);
$scope.$on('$destroy', function() {
deregisterAllocatedWatcher();
});
function hideAllocated(allocatedList) {
var allocatedDict = {};
var availableList;
allocatedList.forEach(function(port) {
allocatedDict[port.id] = true;
});
availableList = $scope.ports.subportCandidates.filter(
function(port) {
return !(port.id in allocatedDict);
function hideAllocated(allocatedList) {
if (!ctrl.portsLoaded || !allocatedList) {
return;
}
);
ctrl.subportsTables.available = availableList;
// Notify transfertable.
$scope.$broadcast(
ttevents.TABLES_CHANGED,
{data: {available: availableList}}
);
}
var allocatedDict = {};
var availableList;
allocatedList.forEach(function(port) {
allocatedDict[port.id] = true;
});
availableList = subportCandidates.filter(
function(port) {
return !(port.id in allocatedDict);
}
);
ctrl.subportsTables.available = availableList;
// Notify transfertable.
$scope.$broadcast(
ttevents.TABLES_CHANGED,
{data: {available: availableList}}
);
}
ctrl.portsLoaded = true;
});
}
})();

View File

@ -23,22 +23,25 @@
beforeEach(module('horizon.app.core.trunks'));
describe('TrunkSubPortsController', function() {
var scope, ctrl, ttevents;
var $q, $timeout, $scope, ctrl;
beforeEach(inject(function($rootScope, $controller, $injector) {
scope = $rootScope.$new();
scope.ports = {
subportCandidates: [{id: 1}, {id: 2}],
subportsOfInitTrunk: []
};
scope.stepModels = {};
scope.initTrunk = {
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
$q = _$q_;
$timeout = _$timeout_;
$scope = $rootScope.$new();
$scope.getPortsWithNets = $q.when([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
$scope.stepModels = {};
var trunk = {
sub_ports: []
};
ttevents = $injector.get('horizon.framework.widgets.transfer-table.events');
$scope.initTrunk = trunk;
$scope.getTrunk = $q.when(trunk);
ctrl = $controller('TrunkSubPortsController', {
$scope: scope
$scope: $scope
});
}));
@ -78,12 +81,17 @@
});
it('uses scope to set table data', function() {
expect(ctrl.subportsTables).toBeDefined();
expect(ctrl.subportsTables.available).toEqual(
[{id: 1}, {id: 2}]);
expect(ctrl.subportsTables.allocated).toEqual([]);
expect(ctrl.subportsTables.displayedAllocated).toEqual([]);
expect(ctrl.subportsTables.displayedAvailable).toEqual([]);
$scope.getPortsWithNets.then(function() {
expect(ctrl.subportsTables).toBeDefined();
expect(ctrl.subportsTables.available).toEqual([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
expect(ctrl.subportsTables.allocated).toEqual([]);
expect(ctrl.subportsTables.displayedAllocated).toEqual([]);
expect(ctrl.subportsTables.displayedAvailable).toEqual([]);
});
$timeout.flush();
});
it('has segmentation types dict', function() {
@ -101,65 +109,82 @@
});
it('should return with subports', function() {
ctrl.subportsTables.allocated = [{id: 3}, {id: 4}, {id: 5}];
ctrl.subportsDetails = {
3: {segmentation_type: 'VLAN', segmentation_id: 100},
4: {segmentation_type: 'VLAN', segmentation_id: 101}
};
var subports = scope.stepModels.trunkSlices.getSubports();
expect(subports).toEqual({
sub_ports: [
{port_id: 3, segmentation_id: 100, segmentation_type: 'VLAN'},
{port_id: 4, segmentation_id: 101, segmentation_type: 'VLAN'}
]
$scope.getPortsWithNets.then(function() {
ctrl.subportsTables.allocated = [{id: 3}, {id: 4}, {id: 5}];
ctrl.subportsDetails = {
3: {segmentation_type: 'VLAN', segmentation_id: 100},
4: {segmentation_type: 'VLAN', segmentation_id: 101}
};
var subports = $scope.stepModels.trunkSlices.getSubports();
expect(subports).toEqual({
sub_ports: [
{port_id: 3, segmentation_id: 100, segmentation_type: 'VLAN'},
{port_id: 4, segmentation_id: 101, segmentation_type: 'VLAN'}
]
});
});
$timeout.flush();
});
it('should remove port from available list if parenttable changes', function() {
spyOn(scope, '$broadcast').and.callThrough();
$scope.getPortsWithNets = $q.when([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''},
{id: 3, admin_state_up: true, device_owner: ''}
]);
$scope.stepModels.allocated = {};
$scope.stepModels.allocated.parentPort = [{id: 3}];
ctrl.subportsTables.available = [{id: 1}, {id: 2}, {id: 3}];
scope.stepModels.allocated.parentPort = [{id: 3}];
$scope.getPortsWithNets.then(function() {
ctrl.portsLoaded = true;
scope.$digest();
expect(scope.$broadcast).toHaveBeenCalledWith(
ttevents.TABLES_CHANGED,
{data: {available: [{id: 1}, {id: 2}]}}
);
expect(ctrl.subportsTables.available).toEqual([{id: 1}, {id: 2}]);
expect(ctrl.subportsTables.available).toEqual([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 2, admin_state_up: true, device_owner: ''}
]);
});
$scope.$digest();
});
it('should add to allocated list the subports of the edited trunk', function() {
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
scope.ports = {
subportCandidates: [{id: 1}, {id: 4}],
subportsOfInitTrunk: [{id: 4, segmentation_id: 2, segmentation_type: 'vlan'}]
};
scope.stepModels = {};
scope.initTrunk = {
sub_ports: [{port_id: 4, segmentation_type: 'vlan', segmentation_id: 2}]
$scope = $rootScope.$new();
$scope.getPortsWithNets = $q.when([
{id: 1, admin_state_up: true, device_owner: ''},
{id: 4, admin_state_up: true, device_owner: '', trunk_id: 1}
]);
$scope.stepModels = {};
var trunk = {
id: 1,
sub_ports: [
{port_id: 4, segmentation_type: 'vlan', segmentation_id: 2}
]
};
$scope.initTrunk = trunk;
$scope.getTrunk = $q.when(trunk);
ctrl = $controller('TrunkSubPortsController', {
$scope: scope
$scope: $scope
});
});
expect(ctrl.subportsDetails).toBeDefined();
expect(ctrl.subportsDetails).toEqual({
4: {
segmentation_id: 2,
segmentation_type: 'vlan'
}
$scope.getTrunk.then(function() {
expect(ctrl.subportsDetails).toBeDefined();
expect(ctrl.subportsDetails).toEqual({
4: {
segmentation_id: 2,
segmentation_type: 'vlan'
}
});
});
var subports = scope.stepModels.trunkSlices.getSubports();
expect(subports).toEqual({
sub_ports: [
{port_id: 4, segmentation_id: 2, segmentation_type: 'vlan'}
]
$scope.getPortsWithNets.then(function() {
var subports = $scope.stepModels.trunkSlices.getSubports();
expect(subports).toEqual({
sub_ports: [
{port_id: 4, segmentation_id: 2, segmentation_type: 'vlan'}
]
});
});
$timeout.flush();
});
});

View File

@ -8,173 +8,181 @@
Optional.
</p>
<transfer-table tr-model="ctrl.subportsTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated>
<table st-table="ctrl.subportsTables.displayedAllocated" st-safe-src="ctrl.subportsTables.allocated"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="rsp-p1" translate>Segmentation Type</th>
<th class="rsp-p1" translate>Segmentation Id</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.subportsTables.allocated.length === 0">
<td colspan="7">
<div class="no-rows-help" translate>
Select items from Available items below
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.subportsTables.displayedAllocated track by item.id"
lr-drag-data="ctrl.subportsTables.displayedAllocated"
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="rsp-p1">
<select id="segmentation_type_{$ $index $}"
ng-init="ctrl.subportsDetails[item.id]['segmentation_type'] = ctrl.segmentationTypes[0]"
ng-options="type for type in ctrl.segmentationTypes"
ng-model="ctrl.subportsDetails[item.id]['segmentation_type']">
</select>
</td>
<td class="rsp-p1" ng-if="ctrl.subportsDetails[item.id]['segmentation_type'] === 'inherit'" translate>
inherit
</td>
<td class="rsp-p1" ng-if="ctrl.subportsDetails[item.id]['segmentation_type'] !== 'inherit'">
<!-- NOTE(bence romsics): We could but do not reject non-integer
segmentation IDs. It does not seem worth to add one more
directive for that effect. -->
<input type="number"
id="segmentation_id_{$ $index $}"
min="{$ ctrl.segmentationTypesDict[ctrl.subportsDetails[item.id]['segmentation_type']][0] $}"
max="{$ ctrl.segmentationTypesDict[ctrl.subportsDetails[item.id]['segmentation_type']][1] $}"
ng-model="ctrl.subportsDetails[item.id]['segmentation_id']" required>
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate" item="item">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="7" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</allocated>
<div ng-if="!ctrl.portsLoaded">
<span translate class="subtitle text-info">
Loading ports... Please Wait
</span>
</div>
<available>
<table st-table="ctrl.subportsTables.displayedAvailable" st-safe-src="ctrl.subportsTables.available"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="search-header" colspan="6">
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
</th>
</tr>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="6">
<div class="no-rows-help" translate>
No available items
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.subportsTables.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate" item="item">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="6" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
<div ng-if="ctrl.portsLoaded">
<transfer-table tr-model="ctrl.subportsTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated>
<table st-table="ctrl.subportsTables.displayedAllocated" st-safe-src="ctrl.subportsTables.allocated"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="rsp-p1" translate>Segmentation Type</th>
<th class="rsp-p1" translate>Segmentation Id</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.subportsTables.allocated.length === 0">
<td colspan="7">
<div class="no-rows-help" translate>
Select items from Available items below
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.subportsTables.displayedAllocated track by item.id"
lr-drag-data="ctrl.subportsTables.displayedAllocated"
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="rsp-p1">
<select id="segmentation_type_{$ $index $}"
ng-init="ctrl.subportsDetails[item.id]['segmentation_type'] = ctrl.segmentationTypes[0]"
ng-options="type for type in ctrl.segmentationTypes"
ng-model="ctrl.subportsDetails[item.id]['segmentation_type']">
</select>
</td>
<td class="rsp-p1" ng-if="ctrl.subportsDetails[item.id]['segmentation_type'] === 'inherit'" translate>
inherit
</td>
<td class="rsp-p1" ng-if="ctrl.subportsDetails[item.id]['segmentation_type'] !== 'inherit'">
<!-- NOTE(bence romsics): We could but do not reject non-integer
segmentation IDs. It does not seem worth to add one more
directive for that effect. -->
<input type="number"
id="segmentation_id_{$ $index $}"
min="{$ ctrl.segmentationTypesDict[ctrl.subportsDetails[item.id]['segmentation_type']][0] $}"
max="{$ ctrl.segmentationTypesDict[ctrl.subportsDetails[item.id]['segmentation_type']][1] $}"
ng-model="ctrl.subportsDetails[item.id]['segmentation_id']" required>
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate" item="item">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="7" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<table st-table="ctrl.subportsTables.displayedAvailable" st-safe-src="ctrl.subportsTables.available"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="search-header" colspan="6">
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
</th>
</tr>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="actions_column"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="6">
<div class="no-rows-help" translate>
No available items
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.subportsTables.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} <span translate>on subnet</span>: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate" item="item">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="6" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>MAC Address</dt>
<dd>{$ item.mac_address $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table>
</div>
</div>