Replaces multi select combos with transfer tables

Implements an angular-python bridge that allows django/horizon forms
to use transfer tables (as seen in other parts of horizon, e.g.:
computes launch instance dialog) as form fields. These fields are
then used to replace the multi select combos boxes in the different
GBPUI dialogs/forms.

Note 1: The add and remove policy rule set actions in group details
"Provided Policy Rule Set" and "Consumed Policy Rule Set" are currently
unaffected. These two tabs do not follow the "standard" horizon method
of adding and removing items through one transfer table; instead,
it uses two different dialogs to carry out each operation separately.
This should be addressed in a separate patchset.

Note 2: This is a bit of a stop gap measure, as horizon is slowly moving
away from native django based dialogs and wizards to AngularJS. The goal
should ultimately be to do the same in GBPUI.

Change-Id: I01c9dc08b1bc35309d62eb3da0bd26f3795867ab
Partial-Bug: 1712814
This commit is contained in:
Marek Lycka 2017-08-24 14:50:38 +02:00
parent f14de1c22a
commit cf76c87cc7
12 changed files with 475 additions and 39 deletions

View File

@ -14,3 +14,6 @@ ADD_INSTALLED_APPS = ['gbpui', ]
PANEL_GROUP = 'GroupPolicyPanels'
PANEL_GROUP_NAME = 'Policy'
PANEL_GROUP_DASHBOARD = 'project'
AUTO_DISCOVER_STATIC_FILES = True
ADD_ANGULAR_MODULES = ['gbpui', ]

View File

@ -16,6 +16,11 @@ from django.forms import TextInput
from django.forms import widgets
from django.utils.safestring import mark_safe
from django.forms.utils import flatatt
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
class DynamicMultiSelectWidget(widgets.SelectMultiple):
@ -84,3 +89,78 @@ class DropdownEditWidget(TextInput):
data_list += '<option value="%s">' % item
data_list += '</datalist>'
return mark_safe(text_html + data_list)
class TransferTableWidget(widgets.SelectMultiple):
actions_list = []
add_item_link = None
max_items = None
allocated_filter = False
allocated_help_text = None
available_help_text = None
no_allocated_text = None
no_available_text = None
def render(self, name, value, attrs=None, choices=()):
# css class currently breaks the layout for some reason,
self.attrs.pop('class', None)
final_attrs = self.build_attrs(attrs, name=name)
selected = [] if value is None else value
options = self.render_options(choices, selected)
if self.add_item_link is not None:
final_attrs['add_item_link'] = urlresolvers.reverse(
self.add_item_link
)
if self.max_items is not None:
final_attrs['max_items'] = self.max_items
if self.allocated_filter:
final_attrs['allocated_filter'] = "True"
final_attrs['allocated_help_text'] = self.allocated_help_text
final_attrs['available_help_text'] = self.available_help_text
final_attrs['no_allocated_text'] = self.no_allocated_text
final_attrs['no_available_text'] = self.no_available_text
open_tag = format_html('<d-table {}>', flatatt(final_attrs))
output = [open_tag, options, '</d-table>']
return mark_safe('\n'.join(output))
# ...this adds the 'add item button' just by existing and returning a
# true-y value
def get_add_item_url(self):
return None
class TransferTableField(fields.MultipleChoiceField):
widget = TransferTableWidget
def __init__(self, add_item_link=None, max_items=-1,
allocated_filter=False,
allocated_help_text="",
available_help_text="",
no_allocated_text=_("Select items from bellow"),
no_available_text=_("No available items"),
*args, **kwargs):
super(TransferTableField, self).__init__(*args, **kwargs)
self.widget.add_item_link = add_item_link
self.widget.max_items = max_items
self.widget.allocated_filter = allocated_filter
self.widget.allocated_help_text = allocated_help_text
self.widget.available_help_text = available_help_text
self.widget.no_allocated_text = no_allocated_text
self.widget.no_available_text = no_available_text
def validate(self, *args, **kwargs):
return True

View File

@ -55,7 +55,7 @@ class BaseUpdateForm(forms.SelfHandlingForm):
class UpdatePolicyRuleSetForm(BaseUpdateForm):
name = forms.CharField(label=_("Name"))
description = forms.CharField(label=_("Description"), required=False)
policy_rules = forms.MultipleChoiceField(label=_("Policy Rules"),)
policy_rules = fields.TransferTableField(label=_("Policy Rules"), )
shared = forms.BooleanField(label=_("Shared"), required=False)
def __init__(self, request, *args, **kwargs):
@ -298,7 +298,11 @@ class UpdatePolicyRuleForm(BaseUpdateForm):
name = forms.CharField(max_length=80, label=_("Name"), required=False)
description = forms.CharField(label=_("Description"), required=False)
policy_classifier_id = forms.ChoiceField(label=_("Policy Classifier"))
policy_actions = forms.MultipleChoiceField(label=_("Policy Actions"))
policy_actions = fields.TransferTableField(
label=_("Policy Actions"),
required=False,
)
shared = forms.BooleanField(label=_("Shared"), required=False)
def __init__(self, request, *args, **kwargs):
@ -306,17 +310,19 @@ class UpdatePolicyRuleForm(BaseUpdateForm):
try:
policyrule_id = self.initial['policyrule_id']
rule = client.policyrule_get(request, policyrule_id)
for item in ['name', 'description',
'policy_classifier_id', 'policy_actions', 'shared']:
self.fields[item].initial = getattr(rule, item)
actions = client.policyaction_list(request,
tenant_id=request.user.tenant_id)
action_list = [a.id for a in actions]
for action in actions:
action.set_id_as_name_if_empty()
actions = sorted(actions, key=lambda action: action.name)
action_list = [(a.id, a.name) for a in actions]
self.fields['policy_actions'].choices = action_list
classifiers = client.policyclassifier_list(request,
tenant_id=request.user.tenant_id)
classifier_list = [(c.id, c.name) for c in classifiers]

View File

@ -21,6 +21,7 @@ from horizon import workflows
from gbpui import client
from gbpui import fields
ADD_POLICY_ACTION_URL = "horizon:project:application_policy:addpolicyaction"
ADD_POLICY_CLASSIFIER_URL = "horizon:project:application_policy:"
ADD_POLICY_CLASSIFIER_URL = ADD_POLICY_CLASSIFIER_URL + "addpolicyclassifier"
@ -28,11 +29,12 @@ ADD_POLICY_RULE_URL = "horizon:project:application_policy:addpolicyrule"
class SelectPolicyRuleAction(workflows.Action):
policy_rules = fields.DynamicMultiChoiceField(
policy_rules = fields.TransferTableField(
label=_("Policy Rules"),
required=False,
add_item_link=ADD_POLICY_RULE_URL,
help_text=_("Create a policy rule set with selected rules."))
help_text=_("Create a policy rule set with selected rules.")
)
class Meta(object):
name = _("Rules")
@ -162,11 +164,17 @@ class SelectPolicyClassifierAction(workflows.Action):
class SelectPolicyActionAction(workflows.Action):
actions = fields.DynamicMultiChoiceField(
actions = fields.TransferTableField(
label=_("Policy Action"),
required=False,
help_text=_("Create a policy-rule with selected action."),
add_item_link=ADD_POLICY_ACTION_URL)
add_item_link=ADD_POLICY_ACTION_URL,
help_text=_("Create a policy-rule with selected action.")
)
def __init__(self, request, context, *args, **kwargs):
super(SelectPolicyActionAction, self).__init__(
request, context, *args, **kwargs
)
class Meta(object):
name = _("actions")
@ -176,14 +184,11 @@ class SelectPolicyActionAction(workflows.Action):
try:
actions = client.policyaction_list(request,
tenant_id=request.user.tenant_id)
action_list = [a.id for a in actions]
for action in actions:
action.set_id_as_name_if_empty()
actions = sorted(actions,
key=lambda action: action.name)
action_list = [(a.id, a.name) for a in actions]
if len(action_list) > 0:
self.fields['actions'].initial = action_list[0]
except Exception as e:
action_list = []
exceptions.handle(request,

View File

@ -63,10 +63,11 @@ class AddL3PolicyForm(forms.SelfHandlingForm):
label=_("Subnet Prefix Length"),
help_text=_("Between 2 - 30 for IP4"
"and 2-127 for IP6."),)
external_segments = \
fields.CustomMultiChoiceField(label=_("External Segments"),
add_item_link=EXT_SEG_PARAM_URL,
required=False)
external_segments = fields.TransferTableField(
label=_("External Segments"),
add_item_link=EXT_SEG_PARAM_URL,
required=False
)
shared = forms.BooleanField(label=_("Shared"),
initial=False,
required=False)
@ -296,9 +297,11 @@ class CreateServicePolicyForm(forms.SelfHandlingForm):
name = forms.CharField(max_length=80, label=_("Name"))
description = forms.CharField(
max_length=80, label=_("Description"), required=False)
network_service_params = fields.CustomMultiChoiceField(label=_(
"Network Service Parameters"), add_item_link=NETWORK_PARAM_URL,
required=False)
network_service_params = fields.TransferTableField(
label=_("Network Service Parameters"),
add_item_link=NETWORK_PARAM_URL,
required=False
)
shared = forms.BooleanField(label=_("Shared"),
initial=False, required=False)
@ -555,9 +558,11 @@ class CreateExternalConnectivityForm(forms.SelfHandlingForm):
"(e.g. 192.168.0.0/24,"
"2001:DB8::/48)"),
version=forms.IPv4 | forms.IPv6, mask=True)
external_routes = fields.CustomMultiChoiceField(
label=_("External Routes"), add_item_link=ROUTE_URL,
required=False)
external_routes = fields.TransferTableField(
label=_("External Routes"),
add_item_link=ROUTE_URL,
required=False
)
subnet_id = forms.ChoiceField(label=_("Subnet ID"), required=False)
port_address_translation = forms.BooleanField(
label=_("Port Address Translation"),

View File

@ -22,6 +22,7 @@ from horizon import forms
from horizon import messages
from gbpui import client
from gbpui import fields
LOG = logging.getLogger(__name__)
@ -31,10 +32,12 @@ class UpdatePolicyTargetForm(forms.SelfHandlingForm):
label=_("Name"), required=False)
description = forms.CharField(max_length=80,
label=_("Description"), required=False)
provided_policy_rule_sets = forms.MultipleChoiceField(
label=_("Provided Policy Rule Set"), required=False)
consumed_policy_rule_sets = forms.MultipleChoiceField(
label=_("Consumed Policy Rule Set"), required=False)
provided_policy_rule_sets = fields.TransferTableField(
label=_("Provided Policy Rule Set"), required=False
)
consumed_policy_rule_sets = fields.TransferTableField(
label=_("Consumed Policy Rule Set"), required=False
)
l2_policy_id = forms.ChoiceField(
label=_("Network Policy"),
required=False,
@ -141,9 +144,9 @@ class UpdateExternalPolicyTargetForm(forms.SelfHandlingForm):
label=_("Name"), required=False)
description = forms.CharField(max_length=80,
label=_("Description"), required=False)
provided_policy_rule_sets = forms.MultipleChoiceField(
provided_policy_rule_sets = fields.TransferTableField(
label=_("Provided Policy Rule Set"), required=False)
consumed_policy_rule_sets = forms.MultipleChoiceField(
consumed_policy_rule_sets = fields.TransferTableField(
label=_("Consumed Policy Rule Set"), required=False)
external_segments = forms.MultipleChoiceField(
label=_("External Connectivity"), required=True,

View File

@ -48,12 +48,12 @@ ADD_EXTERNAL_CONNECTIVITY = \
class SelectPolicyRuleSetAction(workflows.Action):
provided_policy_rule_set = fields.DynamicMultiChoiceField(
provided_policy_rule_set = fields.TransferTableField(
label=_("Provided Policy Rule Set"),
help_text=_("Choose a policy rule set for an Group."),
add_item_link=POLICY_RULE_SET_URL,
required=False)
consumed_policy_rule_set = fields.DynamicMultiChoiceField(
consumed_policy_rule_set = fields.TransferTableField(
label=_("Consumed Policy Rule Set"),
help_text=_("Select consumed policy rule set for Group."),
add_item_link=POLICY_RULE_SET_URL,
@ -64,8 +64,10 @@ class SelectPolicyRuleSetAction(workflows.Action):
help_text = _("Select Policy Rule Set for Group.")
def _policy_rule_set_list(self, request):
policy_rule_sets = client.policy_rule_set_list(request,
tenant_id=request.user.tenant_id)
policy_rule_sets = client.policy_rule_set_list(
request,
tenant_id=request.user.tenant_id
)
for c in policy_rule_sets:
c.set_id_as_name_if_empty()
policy_rule_sets = sorted(policy_rule_sets,
@ -75,12 +77,8 @@ class SelectPolicyRuleSetAction(workflows.Action):
def populate_provided_policy_rule_set_choices(self, request, context):
policy_rule_set_list = []
try:
rsets = self._policy_rule_set_list(request)
if len(rsets) == 0:
rsets.extend([('None', 'No Provided Policy Rule Sets')])
policy_rule_set_list = rsets
policy_rule_set_list = self._policy_rule_set_list(request)
except Exception as e:
policy_rule_set_list = []
msg = _('Unable to retrieve policy rule set. %s.') % (str(e))
exceptions.handle(request, msg)
return policy_rule_set_list
@ -88,9 +86,7 @@ class SelectPolicyRuleSetAction(workflows.Action):
def populate_consumed_policy_rule_set_choices(self, request, context):
policy_rule_set_list = []
try:
policy_rule_set_list = [('None', 'No Consumed Policy Rule Sets')]
policy_rule_set_list =\
self._policy_rule_set_list(request)
policy_rule_set_list = self._policy_rule_set_list(request)
except Exception as e:
msg = _('Unable to retrieve policy rule set. %s.') % (str(e))
exceptions.handle(request, msg)
@ -342,6 +338,7 @@ class SetAccessControlsAction(workflows.Action):
help_text=_("Key pair to use for "
"authentication."),
add_item_link=KEYPAIR_IMPORT_URL)
admin_pass = forms.RegexField(
label=_("Admin Password"),
required=False,

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', ['gbpui.transfer-table-bridge'])
.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,90 @@
/**
* 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.transfer-table-bridge')
.directive('dSelect', function () {
return {
restrict: 'A',
scope: true,
link: function ($scope, $elem, $attrs, $ctrl) {
$.each($scope.tableData.available, function (index, optionObject) {
var option = $("<option></option>");
option.attr("value", optionObject.id);
option.append(optionObject.name);
$elem.append(option);
});
// This change listener watches for changes to the raw
// HTML select element; since the select should be hidden,
// the only possible change is the creation of a new
// option using the horizon add button.
$elem.change(function () {
// Find the last option in the select, since the
// addition is done by Horizon appending the a new
// option element
var option = $(this).find("option").last();
// Create a valid option object and make it available
// at the end of the available list
var val = {
'id': option.attr('value'),
'name': option.text()
};
$scope.tableData.available.push(val);
// Deallocate all the objects using the built in
// transfer table controller deallocation method
var toDeallocate = $scope.tableData.allocated.slice();
$.each(toDeallocate, function (index, object) {
$scope.trCtrl.deallocate(object);
});
// Notify the scope of the deallocations
$scope.$apply();
// Allocate the new option; this mimicks te behaviour
// of the normal Horizon based adding
$scope.trCtrl.allocate(val);
// Notify the scope of the allocation changes
$scope.$apply();
});
// The directive watches for a changes in the allocated
// list to dynamically set values for the hidden element.
$scope.$watchCollection(
function (scope) {
return $scope.tableData.allocated;
},
function (newValue, oldValue) {
var values = $.map(
newValue, function (value, index) {
return value.id;
});
$elem.val(values);
}
);
// Sets initial values as allocated when appropriate
$.each($scope.initial, function (index, initialObject) {
$scope.trCtrl.allocate(initialObject);
});
}
}
});
})();

View File

@ -0,0 +1,91 @@
/**
* 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.transfer-table-bridge')
.directive('dTable', ['gbpui.basePath', function(basePath){
return {
restrict: 'E',
scope: true,
templateUrl: basePath +
"transfer-table-bridge/transfer-table-bridge.html",
transclude: true,
link: function($scope, $elem, $attrs, $ctrl, $transclude) {
var initial = [];
var available = [];
var transcluded = $transclude();
transcluded.each(function(index, element) {
if(element.localName=="option") {
var val = {
'id': $(element).attr('value'),
'name': $(element).text()
};
available.push(val);
if($(element).prop('selected')) {
initial.push(val);
}
}
});
$scope.initial = initial;
var allocated = [];
$scope.tableData = {
available: available,
allocated: allocated,
displayedAvailable: [],
displayedAllocated: [],
minItems: -1
};
var maxAllocation = "maxItems" in $attrs
? Number($attrs["maxItems"])
: -1;
$scope.tableLimits = {
maxAllocation: maxAllocation
};
$scope.tableHelpText = {
allocHelpText: $attrs['allocatedHelpText'],
availHelpText: $attrs['availableHelpText'],
noAllocText: $attrs['noAllocatedText'],
noAvailText: $attrs['noAvailableText']
};
$scope.facets = [{
label: gettext("Name"),
name: "name",
singleton: true
}];
if("addItemLink" in $attrs) {
$scope.addItemLink = $attrs["addItemLink"];
}
if("allocatedFilter" in $attrs) {
$scope.allocatedFilter = true;
}
$scope.id = $attrs["id"];
$scope.name = $attrs["name"];
}
}
}])
})();

View File

@ -0,0 +1,114 @@
<transfer-table tr-model="tableData" limits="tableLimits" help-text="tableHelpText">
<allocated ng-model="tableData.allocated.length">
<table st-table="tableData.displayedAllocated"
st-safe-src="tableData.allocated"
hz-table
class="table table-striped table-rsp table-detail table-condensed">
<thead>
<tr ng-if="allocatedFilter">
<th colspan="2">
<hz-magic-search-bar filter-facets="facets"></hz-magic-search-bar>
</th>
</tr>
<tr>
<th colspan="2">Name</th>
</tr>
</thead>
<tbody>
<tr ng-if="tableData.allocated.length === 0">
<td colspan="{{ addItemLink ? 2 : 1 }}">
<div class="no-rows-help">
{$ ::tableHelpText.noAllocText $}
</div>
</td>
</tr>
<tr ng-repeat="row in tableData.displayedAllocated track by row.id">
<td>
{$ row.name $}
</td>
<td class="actions_column">
<action-list>
<button tabIndex="0"
ng-class="'btn btn-default'"
ng-click="trCtrl.deallocate(row)"
type="button">
<span class="fa fa-arrow-down"></span>
</button>
</action-list>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<table
st-table="tableData.displayedAvailable"
st-safe-src="tableData.available"
hz-table
class="table table-striped table-rsp table-detail table-condensed">
<thead>
<tr>
<th colspan="{$ addItemLink ? 1 : 2 $}">
<hz-magic-search-bar filter-facets="facets"></hz-magic-search-bar>
</th>
<th ng-if="addItemLink">
<span class="input-group-btn">
<a href="{$ addItemLink $}" data-add-to-field="{$ id $}_select" class="btn btn-default ajax-add ajax-modal">
<span class="fa fa-plus"></span>
</a>
</span>
</th>
</tr>
<tr>
<th colspan="2">Name</th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="{{ addItemLink ? 2 : 1 }}">
<div class="no-rows-help">
{$ ::tableHelpText.noAvailText $}
</div>
</td>
</tr>
<tr ng-repeat="row in tableData.displayedAvailable track by row.id"
ng-if="!trCtrl.allocatedIds[row.id]"
>
<td>{$ row.name $}</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>
<button tabIndex="0"
ng-class="'btn btn-default'"
ng-click="trCtrl.allocate(row)"
type="button">
<span class="fa fa-arrow-up"></span>
</button>
</action-list>
</td>
</tr>
</tbody>
</table>
<div style="display:None">
<select
d-select
id="{$ id $}_select"
data-add-item-url
multiple
name="{$ name $}" >
</select>
</div>
</available>
</transfer-table>

View File

@ -0,0 +1,17 @@
/**
* 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.transfer-table-bridge', ['horizon.app.core.workflow']);
})();