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:
@@ -30,7 +30,6 @@
|
|||||||
'horizon.app.core.trunks.actions.ports-extra.service',
|
'horizon.app.core.trunks.actions.ports-extra.service',
|
||||||
'horizon.app.core.trunks.resourceType',
|
'horizon.app.core.trunks.resourceType',
|
||||||
'horizon.framework.util.actions.action-result.service',
|
'horizon.framework.util.actions.action-result.service',
|
||||||
'horizon.framework.widgets.modal-wait-spinner.service',
|
|
||||||
// Using horizon.framework.widgets.form.ModalFormService and
|
// Using horizon.framework.widgets.form.ModalFormService and
|
||||||
// angular-schema-form would have made many things easier, but it wasn't
|
// 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.
|
// really an option because it does not have a transfer-table widget.
|
||||||
@@ -52,7 +51,6 @@
|
|||||||
portsExtra,
|
portsExtra,
|
||||||
resourceType,
|
resourceType,
|
||||||
actionResultService,
|
actionResultService,
|
||||||
spinnerService,
|
|
||||||
wizardModalService,
|
wizardModalService,
|
||||||
toast
|
toast
|
||||||
) {
|
) {
|
||||||
@@ -74,65 +72,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function perform() {
|
function perform() {
|
||||||
// NOTE(bence romsics): Suboptimal UX. We delay opening the modal until
|
// NOTE(bence romsics): The parent and subport selector steps are shared
|
||||||
// neutron objects are loaded. But ideally the steps independent of
|
// by the create and edit workflows, therefore we have to initialize the
|
||||||
// already existing neutron objects (ie. the trunk details step) could
|
// trunk adequately in both cases. That is an empty trunk for create and
|
||||||
// already work while loading neutron stuff in the background.
|
// the trunk to be updated for edit.
|
||||||
spinnerService.showModalSpinner(gettext('Please Wait'));
|
var trunk = {
|
||||||
|
admin_state_up: true,
|
||||||
return $q.all({
|
description: '',
|
||||||
// TODO(bence romsics): Query filters of port and network listings
|
name: '',
|
||||||
// should be aligned. While here it looks like we query all
|
port_id: undefined,
|
||||||
// possible ports and all possible networks this is not really the
|
sub_ports: []
|
||||||
// case. A few calls down in openstack_dashboard/api/neutron.py
|
};
|
||||||
// we have a filterless port listing, but networks are listed
|
return wizardModalService.modal({
|
||||||
// by network_list_for_tenant() which includes some hardcoded
|
workflow: createWorkflow,
|
||||||
// tenant-specific filtering. Therefore here we may get back some
|
submit: submit,
|
||||||
// ports whose networks we don't have. This is only a problem for
|
data: {
|
||||||
// admin and even then it means just missing network and subnet
|
initTrunk: trunk,
|
||||||
// metadata on some ports. But anyway when we want this panel to
|
getTrunk: $q.when(trunk),
|
||||||
// work for admin too, we should fix this.
|
getPortsWithNets: $q.all({
|
||||||
getNetworks: neutron.getNetworks(),
|
getNetworks: neutron.getNetworks(),
|
||||||
getPorts: userSession.get().then(function(session) {
|
getPorts: userSession.get().then(function(session) {
|
||||||
return neutron.getPorts({project_id: session.project_id});
|
return neutron.getPorts({project_id: session.project_id});
|
||||||
})
|
})
|
||||||
}).then(function(responses) {
|
}).then(function(responses) {
|
||||||
var networks = responses.getNetworks.data.items;
|
var networks = responses.getNetworks.data.items;
|
||||||
var ports = responses.getPorts.data.items;
|
var ports = responses.getPorts.data.items;
|
||||||
return {
|
return portsExtra.addNetworkAndSubnetInfo(
|
||||||
parentPortCandidates: portsExtra.addNetworkAndSubnetInfo(
|
ports, networks).sort(portsExtra.cmpPortsByNameAndId);
|
||||||
ports.filter(portsExtra.isParentPortCandidate),
|
})
|
||||||
networks),
|
}
|
||||||
subportCandidates: portsExtra.addNetworkAndSubnetInfo(
|
}).result;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit(stepModels) {
|
function submit(stepModels) {
|
||||||
|
@@ -104,22 +104,17 @@
|
|||||||
|
|
||||||
it('open the modal with the correct parameters', function() {
|
it('open the modal with the correct parameters', function() {
|
||||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||||
spyOn(modalWaitSpinnerService, 'showModalSpinner');
|
|
||||||
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
|
|
||||||
|
|
||||||
service.perform();
|
service.perform();
|
||||||
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Please Wait');
|
|
||||||
$timeout.flush();
|
|
||||||
|
|
||||||
expect(wizardModalService.modal).toHaveBeenCalled();
|
expect(wizardModalService.modal).toHaveBeenCalled();
|
||||||
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
|
|
||||||
|
|
||||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||||
expect(modalArgs.scope).toBeUndefined();
|
expect(modalArgs.scope).toBeUndefined();
|
||||||
expect(modalArgs.workflow).toBeDefined();
|
expect(modalArgs.workflow).toBeDefined();
|
||||||
expect(modalArgs.submit).toBeDefined();
|
expect(modalArgs.submit).toBeDefined();
|
||||||
expect(modalArgs.data.initTrunk).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() {
|
it('should submit create trunk request to neutron', function() {
|
||||||
|
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
editService.$inject = [
|
editService.$inject = [
|
||||||
'$q',
|
'$q',
|
||||||
|
'$location',
|
||||||
'horizon.app.core.openstack-service-api.neutron',
|
'horizon.app.core.openstack-service-api.neutron',
|
||||||
'horizon.app.core.openstack-service-api.policy',
|
'horizon.app.core.openstack-service-api.policy',
|
||||||
'horizon.app.core.openstack-service-api.userSession',
|
'horizon.app.core.openstack-service-api.userSession',
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
'horizon.app.core.trunks.actions.ports-extra.service',
|
'horizon.app.core.trunks.actions.ports-extra.service',
|
||||||
'horizon.app.core.trunks.resourceType',
|
'horizon.app.core.trunks.resourceType',
|
||||||
'horizon.framework.util.actions.action-result.service',
|
'horizon.framework.util.actions.action-result.service',
|
||||||
'horizon.framework.widgets.modal-wait-spinner.service',
|
|
||||||
'horizon.framework.widgets.modal.wizard-modal.service',
|
'horizon.framework.widgets.modal.wizard-modal.service',
|
||||||
'horizon.framework.widgets.toast.service'
|
'horizon.framework.widgets.toast.service'
|
||||||
];
|
];
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
*/
|
*/
|
||||||
function editService(
|
function editService(
|
||||||
$q,
|
$q,
|
||||||
|
$location,
|
||||||
neutron,
|
neutron,
|
||||||
policy,
|
policy,
|
||||||
userSession,
|
userSession,
|
||||||
@@ -49,7 +50,6 @@
|
|||||||
portsExtra,
|
portsExtra,
|
||||||
resourceType,
|
resourceType,
|
||||||
actionResultService,
|
actionResultService,
|
||||||
spinnerService,
|
|
||||||
wizardModalService,
|
wizardModalService,
|
||||||
toast
|
toast
|
||||||
) {
|
) {
|
||||||
@@ -71,48 +71,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function perform(selected) {
|
function perform(selected) {
|
||||||
// See also at perform() in create action.
|
var params = {};
|
||||||
spinnerService.showModalSpinner(gettext('Please Wait'));
|
|
||||||
|
|
||||||
return $q.all({
|
if ($location.url().indexOf('admin') === -1) {
|
||||||
getNetworks: neutron.getNetworks(),
|
params = {project_id: userSession.project_id};
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function submit(stepModels) {
|
||||||
|
@@ -109,22 +109,17 @@
|
|||||||
|
|
||||||
it('open the modal with the correct parameters', function() {
|
it('open the modal with the correct parameters', function() {
|
||||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||||
spyOn(modalWaitSpinnerService, 'showModalSpinner');
|
|
||||||
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
|
|
||||||
|
|
||||||
service.perform({id: 1});
|
service.perform({id: 1});
|
||||||
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Please Wait');
|
|
||||||
$timeout.flush();
|
|
||||||
|
|
||||||
expect(wizardModalService.modal).toHaveBeenCalled();
|
expect(wizardModalService.modal).toHaveBeenCalled();
|
||||||
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
|
|
||||||
|
|
||||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||||
expect(modalArgs.scope).toBeUndefined();
|
expect(modalArgs.scope).toBeUndefined();
|
||||||
expect(modalArgs.workflow).toBeDefined();
|
expect(modalArgs.workflow).toBeDefined();
|
||||||
expect(modalArgs.submit).toBeDefined();
|
expect(modalArgs.submit).toBeDefined();
|
||||||
expect(modalArgs.data.initTrunk).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() {
|
it('should submit edit trunk request to neutron', function() {
|
||||||
|
@@ -43,46 +43,50 @@
|
|||||||
{ label: gettext('Disabled'), value: false }
|
{ label: gettext('Disabled'), value: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
ctrl.trunk = {
|
$scope.getTrunk.then(function(trunk) {
|
||||||
admin_state_up: $scope.initTrunk.admin_state_up,
|
ctrl.trunk = {
|
||||||
description: $scope.initTrunk.description,
|
admin_state_up: trunk.admin_state_up,
|
||||||
name: $scope.initTrunk.name
|
description: trunk.description,
|
||||||
};
|
name: trunk.name
|
||||||
|
};
|
||||||
|
|
||||||
// NOTE(bence romsics): The step controllers are naturally stateful,
|
// NOTE(bence romsics): The step controllers are naturally stateful,
|
||||||
// but the actions should be stateless. However we have to
|
// but the actions should be stateless. However we have to
|
||||||
// get back the captured user input from the step controller to the
|
// get back the captured user input from the step controller to the
|
||||||
// action, because the action makes the neutron call. WizardController
|
// action, because the action makes the neutron call. WizardController
|
||||||
// helps us and passes $scope.stepModels to the actions' submit().
|
// helps us and passes $scope.stepModels to the actions' submit().
|
||||||
// Also note that $scope.stepModels is shared between all workflow
|
// Also note that $scope.stepModels is shared between all workflow
|
||||||
// steps.
|
// steps.
|
||||||
//
|
//
|
||||||
// We roughly follow the example discussed and presented here:
|
// We roughly follow the example discussed and presented here:
|
||||||
// http://lists.openstack.org/pipermail/openstack-dev/2016-July/099368.html
|
// http://lists.openstack.org/pipermail/openstack-dev/2016-July/099368.html
|
||||||
// https://review.openstack.org/345145
|
// https://review.openstack.org/345145
|
||||||
//
|
//
|
||||||
// Though we deviate a bit in the use of stepModels. The example
|
// Though we deviate a bit in the use of stepModels. The example
|
||||||
// has one model object per step, named after the workflow step's
|
// has one model object per step, named after the workflow step's
|
||||||
// form. Instead we treat stepModels as a generic state variable. See
|
// form. Instead we treat stepModels as a generic state variable. See
|
||||||
// the details below.
|
// the details below.
|
||||||
//
|
//
|
||||||
// The trunkSlices closures return a slice of the trunk model which
|
// 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
|
// 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
|
// using closures we can spare the use of watchers and the constant
|
||||||
// recomputation of the trunk slices even in the more complicated
|
// recomputation of the trunk slices even in the more complicated
|
||||||
// other steps.
|
// other steps.
|
||||||
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
||||||
$scope.stepModels.trunkSlices.getDetails = function() {
|
$scope.stepModels.trunkSlices.getDetails = function() {
|
||||||
return ctrl.trunk;
|
return ctrl.trunk;
|
||||||
};
|
};
|
||||||
|
|
||||||
// In order to keep the update action stateless, we pass the old
|
// In order to keep the update action stateless, we pass the old
|
||||||
// state of the trunk down to the step controllers, then back up
|
// state of the trunk down to the step controllers, then back up
|
||||||
// to the update action's submit(). An alternative would be to
|
// to the update action's submit(). An alternative would be to
|
||||||
// eliminate the need for the old state of the trunk at update,
|
// eliminate the need for the old state of the trunk at update,
|
||||||
// at the price of moving the trunk diffing logic from python to
|
// at the price of moving the trunk diffing logic from python to
|
||||||
// javascript (ie. the subports step controller).
|
// javascript (ie. the subports step controller).
|
||||||
$scope.stepModels.initTrunk = $scope.initTrunk;
|
$scope.stepModels.initTrunk = $scope.initTrunk;
|
||||||
|
|
||||||
|
ctrl.trunkLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,17 +21,20 @@
|
|||||||
beforeEach(module('horizon.app.core.trunks'));
|
beforeEach(module('horizon.app.core.trunks'));
|
||||||
|
|
||||||
describe('TrunkDetailsController', function() {
|
describe('TrunkDetailsController', function() {
|
||||||
var scope, ctrl;
|
var $q, $timeout, scope, ctrl;
|
||||||
|
|
||||||
//beforeEach(module('horizon.app.core.trunks.actions'));
|
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
|
||||||
beforeEach(inject(function($rootScope, $injector, $controller) {
|
$q = _$q_;
|
||||||
|
$timeout = _$timeout_;
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
scope.stepModels = {};
|
scope.stepModels = {};
|
||||||
scope.initTrunk = {
|
var trunk = {
|
||||||
admin_state_up: true,
|
admin_state_up: true,
|
||||||
description: '',
|
description: '',
|
||||||
name: 'trunk1'
|
name: 'trunk1'
|
||||||
};
|
};
|
||||||
|
scope.initTrunk = trunk;
|
||||||
|
scope.getTrunk = $q.when(trunk);
|
||||||
|
|
||||||
ctrl = $controller('TrunkDetailsController', {
|
ctrl = $controller('TrunkDetailsController', {
|
||||||
$scope: scope
|
$scope: scope
|
||||||
@@ -45,17 +48,23 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has trunk property', function() {
|
it('has trunk property', function() {
|
||||||
expect(ctrl.trunk).toBeDefined();
|
scope.getTrunk.then(function() {
|
||||||
expect(ctrl.trunk.admin_state_up).toBeDefined();
|
expect(ctrl.trunk).toBeDefined();
|
||||||
expect(ctrl.trunk.admin_state_up).toEqual(true);
|
expect(ctrl.trunk.admin_state_up).toBeDefined();
|
||||||
expect(ctrl.trunk.description).toBeDefined();
|
expect(ctrl.trunk.admin_state_up).toEqual(true);
|
||||||
expect(ctrl.trunk.name).toBeDefined();
|
expect(ctrl.trunk.description).toBeDefined();
|
||||||
|
expect(ctrl.trunk.name).toBeDefined();
|
||||||
|
});
|
||||||
|
$timeout.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return with trunk', function() {
|
it('should return with trunk', function() {
|
||||||
var trunk = scope.stepModels.trunkSlices.getDetails();
|
scope.getTrunk.then(function() {
|
||||||
expect(trunk.name).toEqual('trunk1');
|
var trunk = scope.stepModels.trunkSlices.getDetails();
|
||||||
expect(trunk.admin_state_up).toBe(true);
|
expect(trunk.name).toEqual('trunk1');
|
||||||
|
expect(trunk.admin_state_up).toBe(true);
|
||||||
|
});
|
||||||
|
$timeout.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -7,44 +7,51 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="content">
|
<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 ng-if="ctrl.trunkLoaded">
|
||||||
<div class="row form-group">
|
<div class="selected-source">
|
||||||
<div class="col-xs-8 col-sm-8">
|
<div class="row form-group">
|
||||||
<div class="form-group">
|
<div class="col-xs-8 col-sm-8">
|
||||||
<label class="control-label" for="trunkForm-name">
|
<div class="form-group">
|
||||||
<translate>Name</translate>
|
<label class="control-label" for="trunkForm-name">
|
||||||
</label>
|
<translate>Name</translate>
|
||||||
<input id="trunkForm-name" name="name"
|
</label>
|
||||||
type="text" class="form-control"
|
<input id="trunkForm-name" name="name"
|
||||||
ng-model="ctrl.trunk.name">
|
type="text" class="form-control"
|
||||||
|
ng-model="ctrl.trunk.name">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-xs-4 col-sm-4">
|
||||||
<div class="col-xs-4 col-sm-4">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label class="control-label">
|
||||||
<label class="control-label">
|
<translate>Admin State</translate>
|
||||||
<translate>Admin State</translate>
|
</label>
|
||||||
</label>
|
<div class="form-field">
|
||||||
<div class="form-field">
|
<div class="btn-group">
|
||||||
<div class="btn-group">
|
<label class="btn btn-default"
|
||||||
<label class="btn btn-default"
|
ng-repeat="option in ctrl.trunkAdminStateOptions"
|
||||||
ng-repeat="option in ctrl.trunkAdminStateOptions"
|
ng-model="ctrl.trunk.admin_state_up"
|
||||||
ng-model="ctrl.trunk.admin_state_up"
|
uib-btn-radio="option.value">{$ ::option.label $}</label>
|
||||||
uib-btn-radio="option.value">{$ ::option.label $}</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row form-group">
|
||||||
<div class="row form-group">
|
<div class="col-xs-12 col-sm-12">
|
||||||
<div class="col-xs-12 col-sm-12">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label class="control-label" for="trunkForm-description">
|
||||||
<label class="control-label" for="trunkForm-description">
|
<translate>Description</translate>
|
||||||
<translate>Description</translate>
|
</label>
|
||||||
</label>
|
<input id="trunkForm-description" name="description"
|
||||||
<input id="trunkForm-description" name="description"
|
type="text" class="form-control"
|
||||||
type="text" class="form-control"
|
ng-model="ctrl.trunk.description">
|
||||||
ng-model="ctrl.trunk.description">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
TrunkParentPortController.$inject = [
|
TrunkParentPortController.$inject = [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
'horizon.app.core.trunks.actions.ports-extra.service',
|
||||||
'horizon.app.core.trunks.portConstants',
|
'horizon.app.core.trunks.portConstants',
|
||||||
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
|
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
|
||||||
'horizon.framework.widgets.transfer-table.events'
|
'horizon.framework.widgets.transfer-table.events'
|
||||||
@@ -39,11 +40,13 @@
|
|||||||
|
|
||||||
function TrunkParentPortController(
|
function TrunkParentPortController(
|
||||||
$scope,
|
$scope,
|
||||||
|
portsExtra,
|
||||||
portConstants,
|
portConstants,
|
||||||
tooltipService,
|
tooltipService,
|
||||||
ttevents
|
ttevents
|
||||||
) {
|
) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
var parentPortCandidates;
|
||||||
|
|
||||||
ctrl.portStatuses = portConstants.statuses;
|
ctrl.portStatuses = portConstants.statuses;
|
||||||
ctrl.portAdminStates = portConstants.adminStates;
|
ctrl.portAdminStates = portConstants.adminStates;
|
||||||
@@ -63,61 +66,72 @@
|
|||||||
maxAllocation: 1
|
maxAllocation: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.parentTables = {
|
$scope.getPortsWithNets.then(function(portsWithNets) {
|
||||||
available: $scope.ports.parentPortCandidates,
|
parentPortCandidates = portsWithNets.filter(
|
||||||
allocated: [],
|
portsExtra.isParentPortCandidate);
|
||||||
displayedAvailable: [],
|
|
||||||
displayedAllocated: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// See also in the details step controller.
|
ctrl.parentTables = {
|
||||||
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
available: parentPortCandidates,
|
||||||
$scope.stepModels.trunkSlices.getParentPort = function() {
|
allocated: [],
|
||||||
var trunk = {port_id: $scope.initTrunk.port_id};
|
displayedAvailable: [],
|
||||||
|
displayedAllocated: []
|
||||||
|
};
|
||||||
|
|
||||||
if (ctrl.parentTables.allocated.length in [0, 1]) {
|
// See also in the details step controller.
|
||||||
trunk.port_id = ctrl.parentTables.allocated[0].id;
|
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
||||||
} else {
|
$scope.stepModels.trunkSlices.getParentPort = function() {
|
||||||
// maxAllocation is 1, so this should never happen.
|
var trunk = {port_id: $scope.initTrunk.port_id};
|
||||||
throw new Error('Allocating multiple parent ports is meaningless.');
|
|
||||||
|
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;
|
ctrl.portsLoaded = true;
|
||||||
};
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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'));
|
beforeEach(module('horizon.app.core.trunks'));
|
||||||
|
|
||||||
describe('TrunkParentPortController', function() {
|
describe('TrunkParentPortController', function() {
|
||||||
var scope, ctrl, ttevents;
|
var $q, $timeout, $scope, ctrl;
|
||||||
|
|
||||||
beforeEach(inject(function($rootScope, $controller, $injector) {
|
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
|
||||||
scope = $rootScope.$new();
|
$q = _$q_;
|
||||||
scope.ports = {
|
$timeout = _$timeout_;
|
||||||
parentPortCandidates: [{id: 1}, {id: 2}]
|
$scope = $rootScope.$new();
|
||||||
};
|
$scope.getPortsWithNets = $q.when([
|
||||||
scope.stepModels = {};
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
scope.initTrunk = {
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
|
]);
|
||||||
|
$scope.stepModels = {};
|
||||||
|
$scope.initTrunk = {
|
||||||
port_id: 1
|
port_id: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
ttevents = $injector.get('horizon.framework.widgets.transfer-table.events');
|
|
||||||
|
|
||||||
ctrl = $controller('TrunkParentPortController', {
|
ctrl = $controller('TrunkParentPortController', {
|
||||||
$scope: scope
|
$scope: $scope
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -78,38 +79,54 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('uses scope to set table data', function() {
|
it('uses scope to set table data', function() {
|
||||||
expect(ctrl.parentTables).toBeDefined();
|
$scope.getPortsWithNets.then(function() {
|
||||||
expect(ctrl.parentTables.available).toEqual(
|
expect(ctrl.parentTables).toBeDefined();
|
||||||
[{id: 1}, {id: 2}]);
|
expect(ctrl.parentTables.available).toEqual([
|
||||||
expect(ctrl.parentTables.allocated).toEqual([]);
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
expect(ctrl.parentTables.displayedAllocated).toEqual([]);
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
expect(ctrl.parentTables.displayedAvailable).toEqual([]);
|
]);
|
||||||
|
expect(ctrl.parentTables.allocated).toEqual([]);
|
||||||
|
expect(ctrl.parentTables.displayedAllocated).toEqual([]);
|
||||||
|
expect(ctrl.parentTables.displayedAvailable).toEqual([]);
|
||||||
|
});
|
||||||
|
$timeout.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return with parent port', function() {
|
it('should return with parent port', function() {
|
||||||
ctrl.parentTables.allocated = [{id: 3}];
|
$scope.getPortsWithNets.then(function() {
|
||||||
var trunk = scope.stepModels.trunkSlices.getParentPort();
|
ctrl.parentTables.allocated = [{id: 3}];
|
||||||
expect(trunk.port_id).toEqual(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() {
|
it('should throw exception if more than one port is allocated', function() {
|
||||||
ctrl.parentTables.allocated = [{id: 3}, {id: 4}];
|
$scope.getPortsWithNets.then(function() {
|
||||||
expect(scope.stepModels.trunkSlices.getParentPort).toThrow();
|
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() {
|
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.getPortsWithNets.then(function() {
|
||||||
scope.stepModels.allocated.subports = [{id: 3}];
|
ctrl.portsLoaded = true;
|
||||||
|
|
||||||
scope.$digest();
|
expect(ctrl.parentTables.available).toEqual([
|
||||||
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
expect(scope.$broadcast).toHaveBeenCalledWith(
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
ttevents.TABLES_CHANGED,
|
]);
|
||||||
{data: {available: [{id: 1}, {id: 2}]}}
|
});
|
||||||
);
|
$scope.$digest();
|
||||||
expect(ctrl.parentTables.available).toEqual([{id: 1}, {id: 2}]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -7,159 +7,165 @@
|
|||||||
be created. Mandatory.
|
be created. Mandatory.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<transfer-table tr-model="ctrl.parentTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
<div ng-if="!ctrl.portsLoaded">
|
||||||
<allocated ng-model="ctrl.parentTables.allocated.length" validate-number-min="1">
|
<span translate class="subtitle text-info">
|
||||||
<table st-table="ctrl.parentTables.displayedAllocated" st-safe-src="ctrl.parentTables.allocated"
|
Loading ports... Please Wait
|
||||||
hz-table class="table table-striped table-rsp table-detail">
|
</span>
|
||||||
<thead>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<available>
|
<div ng-if="ctrl.portsLoaded">
|
||||||
<table st-table="ctrl.parentTables.displayedAvailable" st-safe-src="ctrl.parentTables.available"
|
<transfer-table tr-model="ctrl.parentTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
||||||
hz-table class="table table-striped table-rsp table-detail">
|
<allocated ng-model="ctrl.parentTables.allocated.length" validate-number-min="1">
|
||||||
<thead>
|
<table st-table="ctrl.parentTables.displayedAllocated" st-safe-src="ctrl.parentTables.allocated"
|
||||||
<tr>
|
hz-table class="table table-striped table-rsp table-detail">
|
||||||
<th class="search-header" colspan="6">
|
<thead>
|
||||||
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
|
<tr>
|
||||||
</th>
|
<th class="expander"></th>
|
||||||
</tr>
|
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
|
||||||
<tr>
|
<th class="rsp-p2" translate>IP</th>
|
||||||
<th class="expander"></th>
|
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
|
||||||
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
|
<th st-sort="status" class="rsp-p1" translate>Status</th>
|
||||||
<th class="rsp-p2" translate>IP</th>
|
<th class="actions_column"></th>
|
||||||
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
|
</tr>
|
||||||
<th st-sort="status" class="rsp-p1" translate>Status</th>
|
</thead>
|
||||||
<th class="actions_column"></th>
|
<tbody>
|
||||||
</tr>
|
<tr ng-if="ctrl.parentTables.allocated.length === 0">
|
||||||
</thead>
|
<td colspan="7">
|
||||||
<tbody>
|
<div class="no-rows-help" translate>
|
||||||
<tr ng-if="trCtrl.numAvailable() === 0">
|
Select an item from Available items below
|
||||||
<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>
|
</div>
|
||||||
</dl>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
<tr ng-repeat-start="item in ctrl.parentTables.displayedAllocated track by item.id"
|
||||||
</tbody>
|
lr-drag-data="ctrl.parentTables.displayedAllocated"
|
||||||
</table>
|
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
|
||||||
</available>
|
<td class="expander">
|
||||||
</transfer-table>
|
<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>
|
</div>
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
TrunkSubPortsController.$inject = [
|
TrunkSubPortsController.$inject = [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
'horizon.app.core.trunks.actions.ports-extra.service',
|
||||||
'horizon.app.core.trunks.portConstants',
|
'horizon.app.core.trunks.portConstants',
|
||||||
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
|
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
|
||||||
'horizon.framework.widgets.transfer-table.events'
|
'horizon.framework.widgets.transfer-table.events'
|
||||||
@@ -38,11 +39,13 @@
|
|||||||
|
|
||||||
function TrunkSubPortsController(
|
function TrunkSubPortsController(
|
||||||
$scope,
|
$scope,
|
||||||
|
portsExtra,
|
||||||
portConstants,
|
portConstants,
|
||||||
tooltipService,
|
tooltipService,
|
||||||
ttevents
|
ttevents
|
||||||
) {
|
) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
var subportCandidates;
|
||||||
|
|
||||||
ctrl.portStatuses = portConstants.statuses;
|
ctrl.portStatuses = portConstants.statuses;
|
||||||
ctrl.portAdminStates = portConstants.adminStates;
|
ctrl.portAdminStates = portConstants.adminStates;
|
||||||
@@ -71,80 +74,92 @@
|
|||||||
ctrl.segmentationTypes = Object.keys(ctrl.segmentationTypesDict);
|
ctrl.segmentationTypes = Object.keys(ctrl.segmentationTypesDict);
|
||||||
ctrl.subportsDetails = {};
|
ctrl.subportsDetails = {};
|
||||||
|
|
||||||
$scope.initTrunk.sub_ports.forEach(function(subport) {
|
$scope.getTrunk.then(function(trunk) {
|
||||||
ctrl.subportsDetails[subport.port_id] = {
|
trunk.sub_ports.forEach(function(subport) {
|
||||||
segmentation_type: subport.segmentation_type,
|
ctrl.subportsDetails[subport.port_id] = {
|
||||||
segmentation_id: subport.segmentation_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 = {
|
// See also in the details step controller.
|
||||||
available: [].concat(
|
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
||||||
$scope.ports.subportsOfInitTrunk,
|
$scope.stepModels.trunkSlices.getSubports = function() {
|
||||||
$scope.ports.subportCandidates),
|
var trunk = {sub_ports: []};
|
||||||
// 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.
|
ctrl.subportsTables.allocated.forEach(function(port) {
|
||||||
$scope.stepModels.trunkSlices = $scope.stepModels.trunkSlices || {};
|
// Subport information comes from two sources, one handled by
|
||||||
$scope.stepModels.trunkSlices.getSubports = function() {
|
// transfertable, the other from outside of transfertable. We
|
||||||
var trunk = {sub_ports: []};
|
// 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) {
|
return trunk;
|
||||||
// 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
|
// We expose the allocated table directly to the parent port step
|
||||||
// skip the inconsistent cases by the following condition.
|
// controller, so it can set watchers on it and react accordingly...
|
||||||
if (port.id in ctrl.subportsDetails) {
|
$scope.stepModels.allocated = $scope.stepModels.allocated || {};
|
||||||
trunk.sub_ports.push({
|
$scope.stepModels.allocated.subports = ctrl.subportsTables.allocated;
|
||||||
port_id: port.id,
|
|
||||||
segmentation_id: ctrl.subportsDetails[port.id].segmentation_id,
|
// ...and vice versa.
|
||||||
segmentation_type: ctrl.subportsDetails[port.id].segmentation_type
|
var deregisterAllocatedWatcher = $scope.$watchCollection(
|
||||||
});
|
'stepModels.allocated.parentPort', hideAllocated);
|
||||||
}
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
deregisterAllocatedWatcher();
|
||||||
});
|
});
|
||||||
|
|
||||||
return trunk;
|
function hideAllocated(allocatedList) {
|
||||||
};
|
if (!ctrl.portsLoaded || !allocatedList) {
|
||||||
|
return;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ctrl.subportsTables.available = availableList;
|
var allocatedDict = {};
|
||||||
// Notify transfertable.
|
var availableList;
|
||||||
$scope.$broadcast(
|
|
||||||
ttevents.TABLES_CHANGED,
|
allocatedList.forEach(function(port) {
|
||||||
{data: {available: availableList}}
|
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'));
|
beforeEach(module('horizon.app.core.trunks'));
|
||||||
|
|
||||||
describe('TrunkSubPortsController', function() {
|
describe('TrunkSubPortsController', function() {
|
||||||
var scope, ctrl, ttevents;
|
var $q, $timeout, $scope, ctrl;
|
||||||
|
|
||||||
beforeEach(inject(function($rootScope, $controller, $injector) {
|
beforeEach(inject(function(_$q_, _$timeout_, $rootScope, $controller) {
|
||||||
scope = $rootScope.$new();
|
$q = _$q_;
|
||||||
scope.ports = {
|
$timeout = _$timeout_;
|
||||||
subportCandidates: [{id: 1}, {id: 2}],
|
$scope = $rootScope.$new();
|
||||||
subportsOfInitTrunk: []
|
$scope.getPortsWithNets = $q.when([
|
||||||
};
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
scope.stepModels = {};
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
scope.initTrunk = {
|
]);
|
||||||
|
$scope.stepModels = {};
|
||||||
|
var trunk = {
|
||||||
sub_ports: []
|
sub_ports: []
|
||||||
};
|
};
|
||||||
ttevents = $injector.get('horizon.framework.widgets.transfer-table.events');
|
$scope.initTrunk = trunk;
|
||||||
|
$scope.getTrunk = $q.when(trunk);
|
||||||
|
|
||||||
ctrl = $controller('TrunkSubPortsController', {
|
ctrl = $controller('TrunkSubPortsController', {
|
||||||
$scope: scope
|
$scope: $scope
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -78,12 +81,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('uses scope to set table data', function() {
|
it('uses scope to set table data', function() {
|
||||||
expect(ctrl.subportsTables).toBeDefined();
|
$scope.getPortsWithNets.then(function() {
|
||||||
expect(ctrl.subportsTables.available).toEqual(
|
expect(ctrl.subportsTables).toBeDefined();
|
||||||
[{id: 1}, {id: 2}]);
|
expect(ctrl.subportsTables.available).toEqual([
|
||||||
expect(ctrl.subportsTables.allocated).toEqual([]);
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
expect(ctrl.subportsTables.displayedAllocated).toEqual([]);
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
expect(ctrl.subportsTables.displayedAvailable).toEqual([]);
|
]);
|
||||||
|
expect(ctrl.subportsTables.allocated).toEqual([]);
|
||||||
|
expect(ctrl.subportsTables.displayedAllocated).toEqual([]);
|
||||||
|
expect(ctrl.subportsTables.displayedAvailable).toEqual([]);
|
||||||
|
});
|
||||||
|
$timeout.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has segmentation types dict', function() {
|
it('has segmentation types dict', function() {
|
||||||
@@ -101,65 +109,82 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return with subports', function() {
|
it('should return with subports', function() {
|
||||||
ctrl.subportsTables.allocated = [{id: 3}, {id: 4}, {id: 5}];
|
$scope.getPortsWithNets.then(function() {
|
||||||
ctrl.subportsDetails = {
|
ctrl.subportsTables.allocated = [{id: 3}, {id: 4}, {id: 5}];
|
||||||
3: {segmentation_type: 'VLAN', segmentation_id: 100},
|
ctrl.subportsDetails = {
|
||||||
4: {segmentation_type: 'VLAN', segmentation_id: 101}
|
3: {segmentation_type: 'VLAN', segmentation_id: 100},
|
||||||
};
|
4: {segmentation_type: 'VLAN', segmentation_id: 101}
|
||||||
var subports = scope.stepModels.trunkSlices.getSubports();
|
};
|
||||||
expect(subports).toEqual({
|
var subports = $scope.stepModels.trunkSlices.getSubports();
|
||||||
sub_ports: [
|
expect(subports).toEqual({
|
||||||
{port_id: 3, segmentation_id: 100, segmentation_type: 'VLAN'},
|
sub_ports: [
|
||||||
{port_id: 4, segmentation_id: 101, segmentation_type: 'VLAN'}
|
{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() {
|
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.getPortsWithNets.then(function() {
|
||||||
scope.stepModels.allocated.parentPort = [{id: 3}];
|
ctrl.portsLoaded = true;
|
||||||
|
|
||||||
scope.$digest();
|
expect(ctrl.subportsTables.available).toEqual([
|
||||||
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
expect(scope.$broadcast).toHaveBeenCalledWith(
|
{id: 2, admin_state_up: true, device_owner: ''}
|
||||||
ttevents.TABLES_CHANGED,
|
]);
|
||||||
{data: {available: [{id: 1}, {id: 2}]}}
|
});
|
||||||
);
|
$scope.$digest();
|
||||||
expect(ctrl.subportsTables.available).toEqual([{id: 1}, {id: 2}]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add to allocated list the subports of the edited trunk', function() {
|
it('should add to allocated list the subports of the edited trunk', function() {
|
||||||
inject(function($rootScope, $controller) {
|
inject(function($rootScope, $controller) {
|
||||||
scope = $rootScope.$new();
|
$scope = $rootScope.$new();
|
||||||
scope.ports = {
|
$scope.getPortsWithNets = $q.when([
|
||||||
subportCandidates: [{id: 1}, {id: 4}],
|
{id: 1, admin_state_up: true, device_owner: ''},
|
||||||
subportsOfInitTrunk: [{id: 4, segmentation_id: 2, segmentation_type: 'vlan'}]
|
{id: 4, admin_state_up: true, device_owner: '', trunk_id: 1}
|
||||||
};
|
]);
|
||||||
scope.stepModels = {};
|
$scope.stepModels = {};
|
||||||
scope.initTrunk = {
|
var trunk = {
|
||||||
sub_ports: [{port_id: 4, segmentation_type: 'vlan', segmentation_id: 2}]
|
id: 1,
|
||||||
|
sub_ports: [
|
||||||
|
{port_id: 4, segmentation_type: 'vlan', segmentation_id: 2}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
$scope.initTrunk = trunk;
|
||||||
|
$scope.getTrunk = $q.when(trunk);
|
||||||
ctrl = $controller('TrunkSubPortsController', {
|
ctrl = $controller('TrunkSubPortsController', {
|
||||||
$scope: scope
|
$scope: $scope
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctrl.subportsDetails).toBeDefined();
|
$scope.getTrunk.then(function() {
|
||||||
expect(ctrl.subportsDetails).toEqual({
|
expect(ctrl.subportsDetails).toBeDefined();
|
||||||
4: {
|
expect(ctrl.subportsDetails).toEqual({
|
||||||
segmentation_id: 2,
|
4: {
|
||||||
segmentation_type: 'vlan'
|
segmentation_id: 2,
|
||||||
}
|
segmentation_type: 'vlan'
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
$scope.getPortsWithNets.then(function() {
|
||||||
var subports = scope.stepModels.trunkSlices.getSubports();
|
var subports = $scope.stepModels.trunkSlices.getSubports();
|
||||||
expect(subports).toEqual({
|
expect(subports).toEqual({
|
||||||
sub_ports: [
|
sub_ports: [
|
||||||
{port_id: 4, segmentation_id: 2, segmentation_type: 'vlan'}
|
{port_id: 4, segmentation_id: 2, segmentation_type: 'vlan'}
|
||||||
]
|
]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
$timeout.flush();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,173 +8,181 @@
|
|||||||
Optional.
|
Optional.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<transfer-table tr-model="ctrl.subportsTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
<div ng-if="!ctrl.portsLoaded">
|
||||||
<allocated>
|
<span translate class="subtitle text-info">
|
||||||
<table st-table="ctrl.subportsTables.displayedAllocated" st-safe-src="ctrl.subportsTables.allocated"
|
Loading ports... Please Wait
|
||||||
hz-table class="table table-striped table-rsp table-detail">
|
</span>
|
||||||
<thead>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<available>
|
<div ng-if="ctrl.portsLoaded">
|
||||||
<table st-table="ctrl.subportsTables.displayedAvailable" st-safe-src="ctrl.subportsTables.available"
|
<transfer-table tr-model="ctrl.subportsTables" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
||||||
hz-table class="table table-striped table-rsp table-detail">
|
<allocated>
|
||||||
<thead>
|
<table st-table="ctrl.subportsTables.displayedAllocated" st-safe-src="ctrl.subportsTables.allocated"
|
||||||
<tr>
|
hz-table class="table table-striped table-rsp table-detail">
|
||||||
<th class="search-header" colspan="6">
|
<thead>
|
||||||
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
|
<tr>
|
||||||
</th>
|
<th class="expander"></th>
|
||||||
</tr>
|
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
|
||||||
<tr>
|
<th class="rsp-p2" translate>IP</th>
|
||||||
<th class="expander"></th>
|
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
|
||||||
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
|
<th st-sort="status" class="rsp-p1" translate>Status</th>
|
||||||
<th class="rsp-p2" translate>IP</th>
|
<th class="rsp-p1" translate>Segmentation Type</th>
|
||||||
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
|
<th class="rsp-p1" translate>Segmentation Id</th>
|
||||||
<th st-sort="status" class="rsp-p1" translate>Status</th>
|
<th class="actions_column"></th>
|
||||||
<th class="actions_column"></th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
<tr ng-if="ctrl.subportsTables.allocated.length === 0">
|
||||||
<tr ng-if="trCtrl.numAvailable() === 0">
|
<td colspan="7">
|
||||||
<td colspan="6">
|
<div class="no-rows-help" translate>
|
||||||
<div class="no-rows-help" translate>
|
Select items from Available items below
|
||||||
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>
|
</div>
|
||||||
</dl>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
<tr ng-repeat-start="item in ctrl.subportsTables.displayedAllocated track by item.id"
|
||||||
</tbody>
|
lr-drag-data="ctrl.subportsTables.displayedAllocated"
|
||||||
</table>
|
lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
|
||||||
</available>
|
<td class="expander">
|
||||||
</transfer-table>
|
<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>
|
</div>
|
||||||
|
Reference in New Issue
Block a user