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:
parent
0a51ce628d
commit
705c52bf1f
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue