Merge "Add Network Port selection to new instance launch"
This commit is contained in:
commit
2187d2b0c3
@ -119,6 +119,7 @@
|
||||
instance: null
|
||||
},
|
||||
networks: [],
|
||||
ports: [],
|
||||
neutronEnabled: false,
|
||||
novaLimits: {},
|
||||
profiles: [],
|
||||
@ -154,6 +155,7 @@
|
||||
// REQUIRED
|
||||
name: null,
|
||||
networks: [],
|
||||
ports: [],
|
||||
profile: {},
|
||||
// REQUIRED Server Key. May be empty.
|
||||
security_groups: [],
|
||||
@ -254,6 +256,7 @@
|
||||
setFinalSpecBootsource(finalSpec);
|
||||
setFinalSpecFlavor(finalSpec);
|
||||
setFinalSpecNetworks(finalSpec);
|
||||
setFinalSpecPorts(finalSpec);
|
||||
setFinalSpecKeyPairs(finalSpec);
|
||||
setFinalSpecSecurityGroups(finalSpec);
|
||||
setFinalSpecMetadata(finalSpec);
|
||||
@ -359,13 +362,14 @@
|
||||
// Networks
|
||||
|
||||
function getNetworks() {
|
||||
return neutronAPI.getNetworks().then(onGetNetworks, noop);
|
||||
return neutronAPI.getNetworks().then(onGetNetworks, noop).then(getPorts, noop);
|
||||
}
|
||||
|
||||
function onGetNetworks(data) {
|
||||
model.neutronEnabled = true;
|
||||
model.networks.length = 0;
|
||||
push.apply(model.networks, data.data.items);
|
||||
return data;
|
||||
}
|
||||
|
||||
function setFinalSpecNetworks(finalSpec) {
|
||||
@ -380,6 +384,55 @@
|
||||
delete finalSpec.networks;
|
||||
}
|
||||
|
||||
function getPorts(networks) {
|
||||
model.ports.length = 0;
|
||||
networks.data.items.forEach(function(network) {
|
||||
return neutronAPI.getPorts({network_id: network.id}).then(
|
||||
function(ports) {
|
||||
onGetPorts(ports, network);
|
||||
}, noop
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function onGetPorts(networkPorts, network) {
|
||||
var ports = [];
|
||||
networkPorts.data.items.forEach(function(port) {
|
||||
// no device_owner means that the port can be attached
|
||||
if (port.device_owner === "" && port.admin_state === "UP") {
|
||||
port.subnet_names = getPortSubnets(port, network.subnets);
|
||||
port.network_name = network.name;
|
||||
ports.push(port);
|
||||
}
|
||||
});
|
||||
push.apply(model.ports, ports);
|
||||
}
|
||||
|
||||
// helper function to return an object of IP:NAME pairs for subnet mapping
|
||||
function getPortSubnets(port, subnets) {
|
||||
var subnetNames = {};
|
||||
port.fixed_ips.forEach(function (ip) {
|
||||
subnets.forEach(function (subnet) {
|
||||
if (ip.subnet_id === subnet.id) {
|
||||
subnetNames[ip.ip_address] = subnet.name;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return subnetNames;
|
||||
}
|
||||
|
||||
function setFinalSpecPorts(finalSpec) {
|
||||
// nics should already be filled so we only append to it
|
||||
finalSpec.ports.forEach(function (port) {
|
||||
finalSpec.nics.push(
|
||||
{
|
||||
"port-id": port.id
|
||||
});
|
||||
});
|
||||
delete finalSpec.ports;
|
||||
}
|
||||
|
||||
// Boot Source
|
||||
|
||||
function getImages() {
|
||||
|
@ -23,6 +23,53 @@
|
||||
var cinderEnabled = false;
|
||||
var neutronEnabled = false;
|
||||
var novaExtensionsEnabled = false;
|
||||
var novaApi = {
|
||||
createServer: function(finalSpec) {
|
||||
return {
|
||||
then: function () {
|
||||
return finalSpec;
|
||||
}
|
||||
};
|
||||
},
|
||||
getAvailabilityZones: function() {
|
||||
var zones = [
|
||||
{ zoneName: 'zone-1', zoneState: { available: true } },
|
||||
{ zoneName: 'zone-2', zoneState: { available: true } },
|
||||
{ zoneName: 'invalid-zone-1' },
|
||||
{ zoneName: 'invalid-zone-2' }
|
||||
];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: zones } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getFlavors: function() {
|
||||
var flavors = [ 'flavor-1', 'flavor-2' ];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: flavors } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getKeypairs: function() {
|
||||
var keypairs = [ { keypair: { name: 'key-1' } },
|
||||
{ keypair: { name: 'key-2' } } ];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: keypairs } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getLimits: function() {
|
||||
var limits = { maxTotalInstances: 10, totalInstancesUsed: 0 };
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: limits });
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
|
||||
|
||||
@ -61,53 +108,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
$provide.value('horizon.app.core.openstack-service-api.nova', {
|
||||
createServer: function(finalSpec) {
|
||||
return {
|
||||
then: function () {
|
||||
return finalSpec;
|
||||
}
|
||||
};
|
||||
},
|
||||
getAvailabilityZones: function() {
|
||||
var zones = [
|
||||
{ zoneName: 'zone-1', zoneState: { available: true } },
|
||||
{ zoneName: 'zone-2', zoneState: { available: true } },
|
||||
{ zoneName: 'invalid-zone-1' },
|
||||
{ zoneName: 'invalid-zone-2' }
|
||||
];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: zones } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getFlavors: function() {
|
||||
var flavors = [ 'flavor-1', 'flavor-2' ];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: flavors } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getKeypairs: function() {
|
||||
var keypairs = [ { keypair: { name: 'key-1' } },
|
||||
{ keypair: { name: 'key-2' } } ];
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: keypairs } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getLimits: function() {
|
||||
var limits = { maxTotalInstances: 10, totalInstancesUsed: 0 };
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: limits });
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
});
|
||||
$provide.value('horizon.app.core.openstack-service-api.nova', novaApi);
|
||||
|
||||
$provide.value('horizon.app.core.openstack-service-api.security-group', {
|
||||
query: function() {
|
||||
@ -130,6 +131,23 @@
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: networks } });
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getPorts: function(network) {
|
||||
var ports = {
|
||||
'net-1': [
|
||||
{ name: 'port-1', device_owner: '', fixed_ips: [], admin_state: 'UP' },
|
||||
{ name: 'port-2', device_owner: '', fixed_ips: [], admin_state: 'DOWN' }
|
||||
],
|
||||
'net-2': [
|
||||
{ name: 'port-3', device_owner: 'owner', fixed_ips: [], admin_state: 'DOWN' },
|
||||
{ name: 'port-4', device_owner: '', fixed_ips: [], admin_state: 'DOWN' }
|
||||
]
|
||||
};
|
||||
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: ports[network.network_id] } });
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
});
|
||||
@ -351,6 +369,24 @@
|
||||
|
||||
expect(model.newInstanceSpec.config_drive).toBe(true);
|
||||
});
|
||||
|
||||
it('should not set availability zone if the zone list is empty', function () {
|
||||
spyOn(novaApi, 'getAvailabilityZones').and.callFake(function () {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({ data: { items: [] } });
|
||||
return deferred.promise;
|
||||
});
|
||||
model.initialize(true);
|
||||
scope.$apply();
|
||||
expect(model.availabilityZones.length).toBe(0);
|
||||
expect(model.newInstanceSpec.availability_zone).toBe(null);
|
||||
});
|
||||
|
||||
it('sets the ports properly based on device_owner', function () {
|
||||
model.initialize(true);
|
||||
scope.$apply();
|
||||
expect(model.ports.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Post Initialization Model - Initializing', function() {
|
||||
@ -363,7 +399,7 @@
|
||||
// This is here to ensure that as people add/change items, they
|
||||
// don't forget to implement tests for them.
|
||||
it('has the right number of properties', function() {
|
||||
expect(Object.keys(model.newInstanceSpec).length).toBe(18);
|
||||
expect(Object.keys(model.newInstanceSpec).length).toBe(19);
|
||||
});
|
||||
|
||||
it('sets availability zone to null', function() {
|
||||
@ -406,6 +442,10 @@
|
||||
expect(model.newInstanceSpec.networks).toEqual([]);
|
||||
});
|
||||
|
||||
it('sets ports to an empty array', function() {
|
||||
expect(model.newInstanceSpec.ports).toEqual([]);
|
||||
});
|
||||
|
||||
it('sets profile to an empty object', function() {
|
||||
expect(model.newInstanceSpec.profile).toEqual({});
|
||||
});
|
||||
@ -440,6 +480,7 @@
|
||||
model.newInstanceSpec.source = [ { id: 'cirros' } ];
|
||||
model.newInstanceSpec.flavor = { id: 'm1.tiny' };
|
||||
model.newInstanceSpec.networks = [ { id: 'public' }, { id: 'private' } ];
|
||||
model.newInstanceSpec.ports = [ ];
|
||||
model.newInstanceSpec.key_pair = [ { name: 'keypair1' } ];
|
||||
model.newInstanceSpec.security_groups = [ { id: 'adminId', name: 'admin' },
|
||||
{ id: 'demoId', name: 'demo' } ];
|
||||
@ -569,6 +610,19 @@
|
||||
expect(finalSpec.useless).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set final spec in format required if ports are used', function() {
|
||||
model.newInstanceSpec.ports = [{id: 'port1'}];
|
||||
|
||||
var finalSpec = model.createInstance();
|
||||
var finalNetworks = [
|
||||
{ 'net-id': 'public', 'v4-fixed-ip': '' },
|
||||
{ 'net-id': 'private', 'v4-fixed-ip': '' },
|
||||
{ 'port-id': 'port1' }
|
||||
];
|
||||
|
||||
expect(finalSpec.nics).toEqual(finalNetworks);
|
||||
});
|
||||
|
||||
it('provides null for device_name when falsy', function() {
|
||||
model.newInstanceSpec.source_type.type = 'image';
|
||||
model.newInstanceSpec.vol_device_name = false;
|
||||
|
@ -59,6 +59,14 @@
|
||||
formName: 'launchInstanceNetworkForm',
|
||||
requiredServiceTypes: ['network']
|
||||
},
|
||||
{
|
||||
id: 'ports',
|
||||
title: gettext('Network Ports'),
|
||||
templateUrl: basePath + 'networkports/ports.html',
|
||||
helpUrl: basePath + 'networkports/ports.help.html',
|
||||
formName: 'launchInstanceNetworkPortForm',
|
||||
requiredServiceTypes: ['network']
|
||||
},
|
||||
{
|
||||
id: 'secgroups',
|
||||
title: gettext('Security Groups'),
|
||||
|
@ -49,15 +49,16 @@
|
||||
expect(launchInstanceWorkflow.title).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have the eight steps defined', function () {
|
||||
it('should have the nine steps defined', function () {
|
||||
expect(launchInstanceWorkflow.steps).toBeDefined();
|
||||
expect(launchInstanceWorkflow.steps.length).toBe(8);
|
||||
expect(launchInstanceWorkflow.steps.length).toBe(9);
|
||||
|
||||
var forms = [
|
||||
'launchInstanceDetailsForm',
|
||||
'launchInstanceSourceForm',
|
||||
'launchInstanceFlavorForm',
|
||||
'launchInstanceNetworkForm',
|
||||
'launchInstanceNetworkPortForm',
|
||||
'launchInstanceAccessAndSecurityForm',
|
||||
'launchInstanceKeypairForm',
|
||||
'launchInstanceConfigurationForm',
|
||||
@ -72,6 +73,10 @@
|
||||
it('specifies that the network step requires the network service type', function() {
|
||||
expect(launchInstanceWorkflow.steps[3].requiredServiceTypes).toEqual(['network']);
|
||||
});
|
||||
|
||||
it('specifies that the network port step requires the network service type', function() {
|
||||
expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -28,10 +28,11 @@
|
||||
|
||||
LaunchInstanceNetworkController.$inject = [
|
||||
'$scope',
|
||||
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service'
|
||||
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
|
||||
'launchInstanceModel'
|
||||
];
|
||||
|
||||
function LaunchInstanceNetworkController($scope, tooltipService) {
|
||||
function LaunchInstanceNetworkController($scope, tooltipService, launchInstanceModel) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.networkStatuses = {
|
||||
@ -48,7 +49,8 @@
|
||||
available: $scope.model.networks,
|
||||
allocated: $scope.model.newInstanceSpec.networks,
|
||||
displayedAvailable: [],
|
||||
displayedAllocated: []
|
||||
displayedAllocated: [],
|
||||
minItems: 1
|
||||
};
|
||||
|
||||
ctrl.tableLimits = {
|
||||
@ -101,6 +103,28 @@
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function getPorts() {
|
||||
return launchInstanceModel.newInstanceSpec.ports;
|
||||
}
|
||||
|
||||
function toggleNetworksRequirement(newValue) {
|
||||
// if there is a port selected, remove the validate-number-min
|
||||
// for networks table
|
||||
if (newValue.length > 0) {
|
||||
ctrl.tableDataMulti.minItems = 0;
|
||||
}
|
||||
// if no port is selected restore the validate-number-min value
|
||||
if (newValue.length === 0) {
|
||||
ctrl.tableDataMulti.minItems = 1;
|
||||
}
|
||||
}
|
||||
// If a port is selected, then networks are not required
|
||||
var portWatcher = $scope.$watch(getPorts, toggleNetworksRequirement, true);
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
portWatcher();
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -23,22 +23,44 @@
|
||||
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
|
||||
|
||||
describe('LaunchInstanceNetworkController', function() {
|
||||
var scope, ctrl;
|
||||
var scope, ctrl, model;
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
scope = {
|
||||
model: {
|
||||
newInstanceSpec: {
|
||||
networks: ['net-a']
|
||||
},
|
||||
networks: ['net-a', 'net-b']
|
||||
}
|
||||
beforeEach(inject(function($controller, $rootScope) {
|
||||
scope = $rootScope.$new();
|
||||
|
||||
model = {
|
||||
newInstanceSpec: {
|
||||
networks: ['net-a'],
|
||||
ports: []
|
||||
},
|
||||
networks: ['net-a', 'net-b']
|
||||
};
|
||||
|
||||
scope.model = model;
|
||||
|
||||
spyOn(scope, '$watch').and.callThrough();
|
||||
spyOn(scope, '$watchCollection').and.callThrough();
|
||||
|
||||
ctrl = $controller('LaunchInstanceNetworkController', {
|
||||
$scope: scope
|
||||
$scope: scope,
|
||||
launchInstanceModel: model
|
||||
});
|
||||
}));
|
||||
|
||||
it("establishes one watch", function () {
|
||||
expect(scope.$watch.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("changes the network items required based on the ports", function() {
|
||||
expect(ctrl.tableDataMulti.minItems).toEqual(1);
|
||||
model.newInstanceSpec.ports = [{name: "1", id: "1"}];
|
||||
scope.$apply();
|
||||
expect(ctrl.tableDataMulti.minItems).toEqual(0);
|
||||
model.newInstanceSpec.ports = [];
|
||||
scope.$apply();
|
||||
expect(ctrl.tableDataMulti.minItems).toEqual(1);
|
||||
});
|
||||
|
||||
it('has correct network statuses', function() {
|
||||
expect(ctrl.networkStatuses).toBeDefined();
|
||||
expect(ctrl.networkStatuses.ACTIVE).toBeDefined();
|
||||
@ -70,6 +92,7 @@
|
||||
expect(ctrl.tableDataMulti.allocated).toEqual(['net-a']);
|
||||
expect(ctrl.tableDataMulti.displayedAllocated).toEqual([]);
|
||||
expect(ctrl.tableDataMulti.displayedAvailable).toEqual([]);
|
||||
expect(ctrl.tableDataMulti.minItems).toEqual(1);
|
||||
});
|
||||
|
||||
it('should set facets for search', function() {
|
||||
|
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<transfer-table tr-model="ctrl.tableDataMulti" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
||||
<allocated validate-number-min="1" ng-model="ctrl.tableDataMulti.allocated.length">
|
||||
<allocated validate-number-min="{$ ctrl.tableDataMulti.minItems $}" ng-model="ctrl.tableDataMulti.allocated.length">
|
||||
<table st-table="ctrl.tableDataMulti.displayedAllocated" st-safe-src="ctrl.tableDataMulti.allocated" hz-table
|
||||
class="table table-striped table-rsp table-detail">
|
||||
<thead>
|
||||
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name LaunchInstanceNetworkPortController
|
||||
* @description
|
||||
* Controller for the Launch Instance - Network Step.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.workflow.launch-instance')
|
||||
.controller('LaunchInstanceNetworkPortController', LaunchInstanceNetworkPortController);
|
||||
|
||||
LaunchInstanceNetworkPortController.$inject = [
|
||||
'$scope',
|
||||
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service'
|
||||
];
|
||||
|
||||
function LaunchInstanceNetworkPortController($scope, tooltipService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.portStatuses = {
|
||||
'ACTIVE': gettext('Active'),
|
||||
'DOWN': gettext('Down')
|
||||
};
|
||||
|
||||
ctrl.portAdminStates = {
|
||||
'UP': gettext('Up'),
|
||||
'DOWN': gettext('Down')
|
||||
};
|
||||
|
||||
ctrl.vnicTypes = {
|
||||
'normal': gettext('Normal'),
|
||||
'direct': gettext('Direct'),
|
||||
'macvtap': gettext('MacVTap')
|
||||
};
|
||||
|
||||
ctrl.tableDataMulti = {
|
||||
available: $scope.model.ports,
|
||||
allocated: $scope.model.newInstanceSpec.ports,
|
||||
displayedAvailable: [],
|
||||
displayedAllocated: []
|
||||
};
|
||||
|
||||
ctrl.tableLimits = {
|
||||
maxAllocation: -1
|
||||
};
|
||||
|
||||
ctrl.tableHelpText = {
|
||||
allocHelpText: gettext('Select ports from those listed below.')
|
||||
};
|
||||
|
||||
ctrl.tooltipModel = tooltipService;
|
||||
|
||||
ctrl.nameOrID = function nameOrId(data) {
|
||||
return angular.isDefined(data.name) && data.name !== '' ? data.name : data.id;
|
||||
};
|
||||
}
|
||||
})();
|
@ -0,0 +1,23 @@
|
||||
<p translate>
|
||||
A port represents a virtual switch port on a logical network switch.
|
||||
</p>
|
||||
<p translate>
|
||||
Ports can be created under a network by administrators.
|
||||
</p>
|
||||
<p translate>
|
||||
Virtual instances attach their interfaces to ports.
|
||||
</p>
|
||||
<p translate>
|
||||
The logical port also defines the MAC address and the IP address(es) to be assigned to the interfaces plugged into them.
|
||||
</p>
|
||||
<p translate>
|
||||
When IP addresses are associated to a port, this also implies the port is associated with a subnet, as the IP address was taken from the allocation pool for a specific subnet.
|
||||
</p>
|
||||
<p translate>
|
||||
When the <b>Admin State</b> for a port is set to <b>Up</b> and it has no <b>Device Owner</b>,
|
||||
then the port is available for use. You can set the <b>Admin State</b> to <b>Down</b>
|
||||
if you are not ready for other users to use the port.
|
||||
</p>
|
||||
<p translate>
|
||||
The status indicates whether the port has an active connection.
|
||||
</p>
|
@ -0,0 +1,154 @@
|
||||
<div ng-controller="LaunchInstanceNetworkPortController as ctrl">
|
||||
<p translate>
|
||||
Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both.
|
||||
</p>
|
||||
|
||||
<transfer-table tr-model="ctrl.tableDataMulti" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
|
||||
<allocated>
|
||||
<table st-table="ctrl.tableDataMulti.displayedAllocated" st-safe-src="ctrl.tableDataMulti.allocated"
|
||||
hz-table class="table table-striped table-rsp table-detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="reorder"></th>
|
||||
<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="action-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="ctrl.tableDataMulti.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.tableDataMulti.displayedAllocated track by item.id"
|
||||
lr-drag-data="ctrl.tableDataMulti.displayedAllocated" lr-drag-src="reorder"
|
||||
lr-drop-target="reorder" lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
|
||||
<td class="reorder">
|
||||
<span class="fa fa-sort" title="{$ 'Re-order items using drag and drop'|translate $}"></span>
|
||||
{$ $index + 1 $}
|
||||
</td>
|
||||
<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">{$ ctrl.nameOrID(item) $}</td>
|
||||
<td class="rsp-p2">
|
||||
<div ng-repeat="ip in item.fixed_ips">
|
||||
{$ ip.ip_address $} on subnet: {$ 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="action-col">
|
||||
<action-list>
|
||||
<action action-classes="'btn btn-sm btn-default'"
|
||||
callback="trCtrl.deallocate" item="item">
|
||||
<span class="fa fa-minus"></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>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.tableDataMulti.displayedAvailable" st-safe-src="ctrl.tableDataMulti.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="action-col"></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.tableDataMulti.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">{$ ctrl.nameOrID(item) $}</td>
|
||||
<td class="rsp-p2">
|
||||
<div ng-repeat="ip in item.fixed_ips">
|
||||
{$ ip.ip_address $} on subnet: {$ 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="action-col">
|
||||
<action-list>
|
||||
<action action-classes="'btn btn-sm btn-default'"
|
||||
callback="trCtrl.allocate" item="item">
|
||||
<span class="fa fa-plus"></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>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>
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('Launch Instance Ports Step', function() {
|
||||
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.framework.widgets.action-list'));
|
||||
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
|
||||
|
||||
describe('LaunchInstanceNetworkPortController', function() {
|
||||
var scope, ctrl;
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
scope = {
|
||||
model: {
|
||||
newInstanceSpec: {
|
||||
ports: ['port-a']
|
||||
},
|
||||
ports: ['port-a', 'port-b']
|
||||
}
|
||||
};
|
||||
ctrl = $controller('LaunchInstanceNetworkPortController', {
|
||||
$scope: scope
|
||||
});
|
||||
}));
|
||||
|
||||
it('has correct ports statuses', function() {
|
||||
expect(ctrl.portStatuses).toBeDefined();
|
||||
expect(ctrl.portStatuses.ACTIVE).toBeDefined();
|
||||
expect(ctrl.portStatuses.DOWN).toBeDefined();
|
||||
expect(Object.keys(ctrl.portStatuses).length).toBe(2);
|
||||
});
|
||||
|
||||
it('has correct network admin states', function() {
|
||||
expect(ctrl.portAdminStates).toBeDefined();
|
||||
expect(ctrl.portAdminStates.UP).toBeDefined();
|
||||
expect(ctrl.portAdminStates.DOWN).toBeDefined();
|
||||
expect(Object.keys(ctrl.portAdminStates).length).toBe(2);
|
||||
});
|
||||
|
||||
it('defines a multiple-allocation table', function() {
|
||||
expect(ctrl.tableLimits).toBeDefined();
|
||||
expect(ctrl.tableLimits.maxAllocation).toBe(-1);
|
||||
});
|
||||
|
||||
it('contains help text for the table', function() {
|
||||
expect(ctrl.tableHelpText).toBeDefined();
|
||||
expect(ctrl.tableHelpText.allocHelpText).toBeDefined();
|
||||
});
|
||||
|
||||
it('nameOrId return the name', function() {
|
||||
var obj = {name: 'test_name', id: 'test_id'};
|
||||
expect(ctrl.nameOrID).toBeDefined();
|
||||
expect(ctrl.nameOrID(obj)).toBe('test_name');
|
||||
});
|
||||
|
||||
it('nameOrId return the id if the name is missing', function() {
|
||||
expect(ctrl.nameOrID).toBeDefined();
|
||||
expect(ctrl.nameOrID({'id': 'testid'})).toBe('testid');
|
||||
});
|
||||
|
||||
it('uses scope to set table data', function() {
|
||||
expect(ctrl.tableDataMulti).toBeDefined();
|
||||
expect(ctrl.tableDataMulti.available).toEqual(['port-a', 'port-b']);
|
||||
expect(ctrl.tableDataMulti.allocated).toEqual(['port-a']);
|
||||
expect(ctrl.tableDataMulti.displayedAllocated).toEqual([]);
|
||||
expect(ctrl.tableDataMulti.displayedAvailable).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
@ -303,6 +303,7 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('diskFormatFilter', function() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user