Merge "Replaces multi select combos with transfer tables"

This commit is contained in:
Zuul 2017-09-28 23:54:15 +00:00 committed by Gerrit Code Review
commit 84cac65360
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']);
})();