Unifies the compute and GBP member launch modals

Modifies the create member button in group details to use the compute launch
instance AngularJS based launch dialog. The dialog itself is changed through
the Angular API to replace standard networking pages with a group setting page.

Change-Id: Ib9f5b601939ed9e8258e4eab2f2180efc98378ae
Closes-Bug: 1582457
This commit is contained in:
Marek Lycka 2017-08-24 09:54:04 +02:00
parent 84cac65360
commit d86c6ea3cc
19 changed files with 944 additions and 92 deletions

View File

@ -17,3 +17,4 @@ PANEL_GROUP_DASHBOARD = 'project'
AUTO_DISCOVER_STATIC_FILES = True
ADD_ANGULAR_MODULES = ['gbpui', ]
ADD_SCSS_FILES = ['dashboard/gbpui/group-member/group-member.scss']

View File

@ -0,0 +1,219 @@
# 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.
import re
from django.conf import settings
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from openstack_dashboard import api
from openstack_dashboard.api.rest import utils as rest_utils
from horizon import exceptions
from gbpui import client
from netaddr import IPAddress
from netaddr import IPNetwork
import logging
LOG = logging.getLogger(__name__)
class PolicyTargets(generic.View):
# todo: This is a direct port from the old form version; it needs to be
# tested and revised; It may be dodgy and/or redundant because:
# 1) The created regex might be nonsenseical (needs testing)
# 2) The proxy_group_ids might be logical duplication of the third
# third conditional
@staticmethod
def is_proxy_group(policy_target, proxy_group_ids):
if hasattr(settings, 'GBPUI_HIDE_PTG_NAMES_FROM_MEMBER_CREATE'):
regex = "(" + ")|(".join(
settings.GBPUI_HIDE_PTG_NAMES_FROM_MEMBER_CREATE) \
+ ")"
if re.match(regex, policy_target.get('name')):
return True
if policy_target.id in proxy_group_ids:
return True
if policy_target.get('proxied_group_id'):
return True
return False
@rest_utils.ajax()
def get(self, request):
policy_targets = client.policy_target_list(
request, tenant_id=request.user.tenant_id
)
proxy_group_ids = [pt.get('proxy_group_id') for pt in policy_targets
if pt.get('proxy_group_id')]
try:
policy_target_objects = []
for policy_target in policy_targets:
if not self.is_proxy_group(policy_target, proxy_group_ids):
subnet_objects = []
for subnet_id in policy_target.subnets:
try:
subnet = api.neutron.subnet_get(request, subnet_id)
allocation_pool_objects = []
allocation_pools = subnet['allocation_pools']
if allocation_pools:
for allocation_pool in allocation_pools:
allocation_pool_object = {
"start": allocation_pool['start'],
"end": allocation_pool['end']
}
allocation_pool_objects.append(
allocation_pool_object)
subnet_object = {
"cidr": subnet['cidr'],
"allocation_pools": allocation_pool_objects
}
subnet_objects.append(subnet_object)
except Exception:
LOG.exception("Unable to retrieve subnet.")
policy_target_object = {
"id": policy_target.id,
"name_or_id": policy_target.name_or_id,
"subnets": subnet_objects
}
policy_target_objects.append(policy_target_object)
return rest_utils.JSONResponse(policy_target_objects)
except Exception:
msg = _("Failed to retrieve groups")
LOG.error(msg)
exceptions.handle(request, msg, redirect=shortcuts.redirect)
class Members(generic.View):
optional_arguments = [
'block_device_mapping',
'block_device_mapping_v2',
'availability_zone',
'admin_pass', 'disk_config',
'config_drive'
]
@rest_utils.ajax()
def post(self, request):
instance_count = request.DATA['instance_count']
try:
if instance_count == 1:
self.create_instance(request)
elif instance_count > 1:
for i in range(0, instance_count):
self.create_instance(request, "_" + str(i))
except Exception:
instance_name = request.DATA['name']
error = _("Unable to launch member %(count)s with name %(name)s")
message = error % {
'count': instance_count,
'name': instance_name
}
LOG.exception(message)
raise rest_utils.AjaxError(400, message)
return rest_utils.CreatedResponse('/api/nova/servers/%s')
def create_instance(self, request, suffix=""):
# Instances need to be created one by one, because each instance
# needs to have it's own GBP port
kw = {
'instance_count': 1
}
# Mandatory creation arguments and port creation
try:
instance_name = request.DATA['name'] + suffix
meta_data, nics = self.create_ports(request, instance_name)
kw['meta'] = meta_data
kw['nics'] = nics
args = (
request,
instance_name,
request.DATA['source_id'],
request.DATA['flavor_id'],
request.DATA['key_name'],
request.DATA['user_data'],
request.DATA['security_groups'],
)
except KeyError as e:
raise rest_utils.AjaxError(400, 'Missing required parameter '
"'%s'" % e.args[0])
# Optional creation arguments
for name in self.optional_arguments:
if name in request.DATA:
kw[name] = request.DATA[name]
return api.nova.server_create(*args, **kw)
# 1) Missing request.DATA entries get propagated to 'create_instance' as
# KeyError
# 2) All other errors are propagated to 'post' as generic failure Exception
@staticmethod
def create_ports(request, instance_name):
nics = []
pts = []
for policy_target_id in request.DATA["group_policy_targets"]:
policy_target = client.policy_target_get(request,
policy_target_id['id'])
args = {
'policy_target_group_id': policy_target.id,
'name': instance_name[:41] + "_gbpui"
}
for subnet_id in policy_target.subnets:
subnet = api.neutron.subnet_get(request, subnet_id)
if 'fixed_ip' in policy_target_id and IPAddress(
policy_target_id['fixed_ip']) in \
IPNetwork(subnet['cidr']):
args['fixed_ips'] = [{
'subnet_id': subnet['id'],
'ip_address': policy_target_id['fixed_ip']
}]
port = client.pt_create(request, **args)
nics.append({
'port-id': port.port_id
})
pts.append(port.id)
meta_data = {'pts': ','.join(pts)}
return meta_data, nics

View File

@ -136,14 +136,30 @@ class ExternalPTGsTable(tables.DataTable):
class LaunchVMLink(tables.LinkAction):
name = "launch_vm"
name = "launch_vm-ng"
verbose_name = _("Create Member")
classes = ("ajax-modal", "btn-addvm",)
url = "horizon:project:policytargets:policy_targetdetails"
ajax = False
classes = ("btn-launch", )
def get_link_url(self):
return reverse("horizon:project:policytargets:addvm",
kwargs={'policy_target_id':
self.table.kwargs['policy_target_id']})
def get_default_attrs(self):
url_kwargs = {
'policy_target_id': self.table.kwargs['policy_target_id']
}
url = reverse(self.url, kwargs=url_kwargs)
ngclick = "modal.openLaunchInstanceWizard(" \
"{ successUrl: '%s'," \
" defaults: ['%s'] }" \
")" % (url, self.table.kwargs['policy_target_id'])
self.attrs.update({
'ng-controller': 'LaunchInstanceModalController as modal',
'ng-click': ngclick
})
return super(LaunchVMLink, self).get_default_attrs()
def get_link_url(self, datum=None):
return ""
class RemoveVMLink(tables.DeleteAction):

View File

@ -10,11 +10,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url # noqa
import views
import restApi
urlpatterns = [
url(r'^$',
views.IndexView.as_view(),
@ -37,8 +38,6 @@ urlpatterns = [
url(r'^ext_policy_target/(?P<ext_policy_target_id>[^/]+)/$',
views.ExternalPTGDetailsView.as_view(),
name='ext_policy_targetdetails'),
url(r'^addvm/(?P<policy_target_id>[^/]+)/$',
views.LaunchVMView.as_view(), name='addvm'),
url(r'^ext_add_policy_rule_set/(?P<ext_policy_target_id>[^/]+)/$',
views.ExtAddProvidedPRSView.as_view(),
name='ext_add_provided_prs'),
@ -66,4 +65,11 @@ urlpatterns = [
url(r'/check_ip_availability',
views.check_ip_availability,
name='check_ip_availability'),
# Rest APIs for use with AJAX/ANGULARJS calls
url(r'policy_target_groups/$',
restApi.PolicyTargets.as_view(),
name='policy_target_groups'),
url(r'launch_instance/$',
restApi.Members.as_view(),
name='launch_instance'),
]

View File

@ -88,14 +88,16 @@ class ExternalPTGDetailsView(tabs.TabbedTableView):
return context
'''
class LaunchVMView(workflows.WorkflowView):
workflow_class = policy_target_workflows.LaunchInstance
def get_initial(self):
initial = super(LaunchVMView, self).get_initial()
initial = super( LaunchVMView, self).get_initial()
initial['project_id'] = self.request.user.tenant_id
initial['user_id'] = self.request.user.id
return initial
'''
class UpdatePTGView(gbforms.HelpTextModalMixin,

View File

@ -1,16 +1,16 @@
#nodeListSortContainer,#nodeListIdContainer{
width:100%;
#nodeListSortContainer, #nodeListIdContainer {
width: 100%;
}
#selected_node {
margin-bottom: 1.5em;
counter-reset: v1 0;
background: #edf9ff;
border: 1px solid #c0d9e4;
margin-bottom: 1.5em;
counter-reset: v1 0;
background: #edf9ff;
border: 1px solid #c0d9e4;
}
#selected_node li {
position: relative;
position: relative;
}
#selected_node li a.btn:before {
@ -18,87 +18,103 @@
}
#selected_node li:before {
content: "Node:" counter(v1);
counter-increment: v1;
display: inline-block;
margin-right: 5px;
background: #555555;
color: #ffffff;
font-size: 90%;
padding: 0px 4px;
vertical-align: middle;
border-radius: 2px;
position: absolute;
left: -4em;
content: "Node:" counter(v1);
counter-increment: v1;
display: inline-block;
margin-right: 5px;
background: #555555;
color: #ffffff;
font-size: 90%;
padding: 0px 4px;
vertical-align: middle;
border-radius: 2px;
position: absolute;
left: -4em;
}
#selected_node.dragging li:before {
content: "Node:";
background-color: rgba(102, 102, 102, 0.5);
padding-right: 10px;
content: "Node:";
background-color: rgba(102, 102, 102, 0.5);
padding-right: 10px;
}
#selected_node.dragging li.ui-state-highlight:before {
content: "";
background: transparent;
content: "";
background: transparent;
}
.nodelist {
padding: 6px;
background: #eee;
border: 1px solid #dddddd;
min-height: 2em;
width: auto !important;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
.nodelist li {
width: 226px;
list-style-type: none;
margin: 6px auto;
padding: 3px;
background: #ffffff;
border: 1px solid #aaa;
line-height: 18px;
border-radius: 3px;
cursor: move;
padding-left: 23px;
background: #ffffff url('../img/drag.png?93ec7e23f795') no-repeat 11px 50%; }
.nodelist li em {
font-size: 0.5em;
line-height: 1em;
color: #999;
font-style: normal;
margin-left: 0.8em; }
.nodelist li i {
margin-right: 5px;
vertical-align: middle; }
.nodelist li a.btn {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
font-size: 11px;
line-height: 12px;
padding: 2px 5px 3px;
margin-right: 1px;
width: 18px;
text-align: center;
right: 5px;
vertical-align: middle;
float: right; }
.nodelist li a.btn:before {
content: "+"; }
.nodelist li.ui-sortable-helper {
background-color: #def; }
.nodelist li.ui-state-highlight {
border: 1px dotted #cccccc;
background: #efefef;
height: 0.5em; }
.nodelist li:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0; }
padding: 6px;
background: #eee;
border: 1px solid #dddddd;
min-height: 2em;
width: auto !important;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.nodelist li {
width: 226px;
list-style-type: none;
margin: 6px auto;
padding: 3px;
background: #ffffff;
border: 1px solid #aaa;
line-height: 18px;
border-radius: 3px;
cursor: move;
padding-left: 23px;
background: #ffffff url('../img/drag.png?93ec7e23f795') no-repeat 11px 50%;
}
.nodelist li em {
font-size: 0.5em;
line-height: 1em;
color: #999;
font-style: normal;
margin-left: 0.8em;
}
.nodelist li i {
margin-right: 5px;
vertical-align: middle;
}
.nodelist li a.btn {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
font-size: 11px;
line-height: 12px;
padding: 2px 5px 3px;
margin-right: 1px;
width: 18px;
text-align: center;
right: 5px;
vertical-align: middle;
float: right;
}
.nodelist li a.btn:before {
content: "+";
}
.nodelist li.ui-sortable-helper {
background-color: #def;
}
.nodelist li.ui-state-highlight {
border: 1px dotted #cccccc;
background: #efefef;
height: 0.5em;
}
.nodelist li:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}

View File

@ -13,7 +13,7 @@
*/
(function () {
angular
.module('gbpui', ['gbpui.transfer-table-bridge'])
.module('gbpui', ['gbpui.transfer-table-bridge', 'gbpui.group-member'])
.config(module_config);
module_config.$inject = ["$provide","$windowProvider"];

View File

@ -0,0 +1,47 @@
/**
* 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 () {
angular
.module('gbpui.group-member')
.factory('gbpui.group-member.gbp-api', gbpApi);
gbpApi.$inject = [
'horizon.framework.util.http.service',
'horizon.framework.widgets.toast.service'
];
function gbpApi(apiService, toastService) {
function getPolicyTargetGroups() {
return apiService.get('project/policytargets/policy_target_groups')
.error(function () {
toastService.add('error', gettext('Unable to retrieve Policy Target Groups'))
});
}
function createServer(newServer) {
return apiService
.post('project/policytargets/launch_instance/', newServer)
.error(function () {
toastService.add('error', gettext('Unable to create Instance'))
})
}
return {
getPolicyTargetGroups: getPolicyTargetGroups,
createServer: createServer
};
}
})();

View File

@ -0,0 +1,34 @@
/**
* 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.
*/
// Workaround service that allows for the modal launchContext object to be used
// in any controller/service.
(function () {
'use strict';
angular
.module('gbpui.group-member')
.factory('gbpui.group-member.launch-context.service', launchContextService);
launchContextService.$inject = [];
function launchContextService() {
return {
launchContext: {
defaults: []
}
};
}
})();

View File

@ -0,0 +1,71 @@
/**
* 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';
angular
.module('gbpui.group-member')
.config(launchInstanceModelPatcher);
launchInstanceModelPatcher.$inject = ['$provide'];
function launchInstanceModelPatcher($provide) {
$provide.decorator(
'launchInstanceModel',
['$delegate', 'gbpui.group-member.gbp-api', '$q',
'gbpui.group-member.launch-context.service',
function($delegate, gbpApi, $q, launchContextService) {
var createInstance = $delegate.createInstance;
$delegate.createInstance = function() {
// This is a workaround for new instance initalization
// inside 'launchInstanceModel' done by functions not
// exposed by the $delegate
$delegate.newInstanceSpec.group_policy_targets =
$delegate.allocated_group_policy_targets;
return createInstance();
};
$delegate.group_policy_targets = [];
$delegate.allocated_group_policy_targets = [];
var initialize = $delegate.initialize;
$delegate.initialize = function(deep) {
$delegate.group_policy_targets.length = 0;
$delegate.allocated_group_policy_targets.length = 0;
$q.all([gbpApi.getPolicyTargetGroups().then(function(data){
var defaults =
launchContextService.launchContext.defaults;
angular.forEach(data.data, function(value, index) {
$delegate.group_policy_targets.push(value);
if(defaults.indexOf(value.id) > -1) {
$delegate.allocated_group_policy_targets
.push(value);
}
});
}), initialize(deep)
]);
};
return $delegate
}]
)
}
})();

View File

@ -0,0 +1,41 @@
/**
* 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';
angular.module('gbpui.group-member')
.config(launchInstanceServicePatcher);
launchInstanceServicePatcher.$inject = ['$provide'];
function launchInstanceServicePatcher($provide) {
$provide.decorator(
'horizon.dashboard.project.workflow.launch-instance.modal.service',
['$delegate', 'gbpui.group-member.launch-context.service', function(
$delegate, launchContextService
) {
var open = $delegate.open;
$delegate.open = function(launchContext) {
angular.extend(launchContextService.launchContext,
launchContext);
return open(launchContext);
};
return $delegate
}]
);
}
})();

View File

@ -0,0 +1,65 @@
/**
* 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';
angular
.module('gbpui.group-member')
.config(launchInstancePatcher);
launchInstancePatcher.$inject = ["$provide", 'gbpui.basePath'];
function launchInstancePatcher($provide, basePath) {
$provide.decorator(
'horizon.dashboard.project.workflow.launch-instance.workflow',
["$delegate", 'horizon.app.core.workflow.factory',
function($delegate, dashboardWorkflow)
{
var steps = $delegate.steps;
var gbstep = {
id: 'gbp',
title: gettext('GBP'),
templateUrl: basePath + 'group-member/group-member.html',
helpUrl: basePath + 'group-member/group-member.help.html',
formName: 'gbpForm'
};
// Finds and replaces the Network and Port wizard pages with
// the GBP wizard page
var networkIndex = -1;
var portIndex = -1;
angular.forEach(steps, function (step) {
if(step.id == 'networks') {
networkIndex = steps.indexOf(step)
} else if(step.id == 'ports') {
portIndex = steps.indexOf(step);
}
});
if(networkIndex > -1) {
steps.splice(networkIndex, 1, gbstep);
}
if(portIndex > -1) {
steps.splice(portIndex, 1);
}
var result = dashboardWorkflow($delegate);
return result;
}]
);
}
})();

View File

@ -0,0 +1,39 @@
/**
* 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';
angular
.module('gbpui.group-member')
.config(novaApiPatcher);
novaApiPatcher.$inject = ['$provide'];
function novaApiPatcher($provide) {
$provide.decorator(
'horizon.app.core.openstack-service-api.nova',
['$delegate', 'gbpui.group-member.gbp-api',
function($delegate, gbpApi) {
var createServer = $delegate.createServer;
$delegate.createServer = function(newServer) {
return gbpApi.createServer(newServer);
};
return $delegate
}]
);
}
})();

View File

@ -0,0 +1,64 @@
/**
* 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() {
angular
.module('gbpui.group-member')
.controller('GBPController', gbpController);
gbpController.$inject = [
'$scope',
'horizon.dashboard.project.workflow.launch-instance.modal.service',
'gbpui.group-member.launch-context.service'
];
function gbpController(
$scope, launchInstanceModalService, launchContextService)
{
var ctrl = this;
ctrl.launchContext = launchContextService.launchContext;
ctrl.inputIPs = {
};
ctrl.tableHelp = {
noneAllocText: gettext('Select a group-member from the table below'),
availHelpText: gettext('Select one or more groups'),
noneAvailText: gettext('No groups available'),
};
ctrl.tableLimits = {
maxAllocation: -1
};
ctrl.addFixedIp = function(row) {
var ipAddress = ctrl.inputIPs[row.id];
row.fixed_ip = ipAddress;
};
ctrl.removeFixedIp = function(row) {
delete row['fixed_ip'];
};
ctrl.tableData = {
available: $scope.model.group_policy_targets,
allocated: $scope.model.allocated_group_policy_targets,
displayedAvailable: [],
displayedAllocated: [],
minItems: 1
};
}
})();

View File

@ -0,0 +1,7 @@
<div>
<p translate>
Instances belonging to network groups will automatically have network
policy rule sets applied to them as defined in the individual network
groups.
</p>
</div>

View File

@ -0,0 +1,187 @@
<div ng-controller="GBPController as ctrl">
<p class="step-description" translate>Select one or more policy groups for the new instance.</p>
<transfer-table tr-model="ctrl.tableData" limits="ctrl.tableLimits"
help-text="ctrl.tableHelp">
<allocated ng-model="ctrl.tableData.allocated.length"
validate-number-min="{$ ctrl.tableData.minItems $}">
<table st-table="ctrl.tableData.allocated"
hz-table
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th>Groups</th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.tableData.allocated.length === 0">
<td colspan="8">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.tableData.allocated track by row.id">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td>{$ row.name_or_id $} <span ng-if="row.fixed_ip">({$ row.fixed_ip $})</span>
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate"
item="row">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="9" class="detail">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<span class="ng-scope">Subnets</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-bordered text-center subnet-table">
<thead>
<tr>
<th rowspan="2">
CIDR
</th>
<th colspan="2">
Allocation pools
</th>
</tr>
<tr>
<th>Start</th>
<th>End</th>
</tr>
</thead>
<tbody>
<tr ng-repeat-start="subnet in row.subnets">
<td>
{$ subnet.cidr $}
</td>
<td>
{$
subnet.allocation_pools.length
> 0 ?
subnet.allocation_pools[0].start
: '' $}
</td>
<td>
{$
subnet.allocation_pools.length
> 0 ?
subnet.allocation_pools[0].end
: '' $}
</td>
</tr>
<tr ng-repeat="allocation_pool in subnet.allocation_pools | limitTo: (1 - subnet.allocation_pools.length)">
<td></td>
<td>
{$ allocation_pool.start $}
</td>
<td>
{$ allocation_pool.end $}
</td>
</tr>
<tr ng-if="0" ng-repeat-end>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<span class="ng-scope">Fixed IP</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form class="form-inline">
<div class="form-group">
<input class="form-control"
type="text"
ng-model="ctrl.inputIPs[row.id]">
<input class="form-control"
type="button" value="Set"
ng-click="ctrl.addFixedIp(row)">
<input class="form-control"
type="button" value="Unset"
ng-click="ctrl.removeFixedIp(row)">
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<table st-table="ctrl.tableData.available"
hz-table
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th>Groups</th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="8">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.tableData.available track by row.id"
ng-if="!trCtrl.allocatedIds[row.id]">
<td>{$ row.name_or_id $}</td>
<td class="actions_column">
<action-list button-tooltip="row.warningMessage"
bt-model="ctrl.tooltipModel"
bt-disabled="!row.disabled"
warning-classes="'invalid'">
<notifications>
<span class="fa fa-exclamation-circle invalid"
ng-show="row.disabled"></span>
</notifications>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate"
item="row"
disabled="row.disabled">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
</tr>
</tbody>
</table>
</available>
</transfer-table>
</div>

View File

@ -0,0 +1,25 @@
/**
* 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(){
angular
.module('gbpui.group-member', [])
.config(module_config);
module_config.$inject = ["$provide","$windowProvider"];
function module_config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'dashboard/gbpui/';
$provide.constant('gbpui.basePath', path);
}
})();

View File

@ -0,0 +1,12 @@
.subnet-table {
width: 100%;
}
.subnet-table td, .subnet-table th {
background-color: white;
text-align: center;
}
.subnet-table > thead > tr > th {
padding:2px;
}