Merge "Horizon selects are now themable: Volumes"
This commit is contained in:
commit
4e384db0cf
@ -35,6 +35,9 @@ from horizon.forms.fields import MultiIPField # noqa
|
|||||||
from horizon.forms.fields import SelectWidget # noqa
|
from horizon.forms.fields import SelectWidget # noqa
|
||||||
from horizon.forms.fields import ThemableCheckboxInput # noqa
|
from horizon.forms.fields import ThemableCheckboxInput # noqa
|
||||||
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
|
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
|
||||||
|
from horizon.forms.fields import ThemableChoiceField # noqa
|
||||||
|
from horizon.forms.fields import ThemableDynamicChoiceField # noqa
|
||||||
|
from horizon.forms.fields import ThemableSelectWidget # noqa
|
||||||
from horizon.forms.views import ModalFormMixin # noqa
|
from horizon.forms.views import ModalFormMixin # noqa
|
||||||
from horizon.forms.views import ModalFormView # noqa
|
from horizon.forms.views import ModalFormView # noqa
|
||||||
|
|
||||||
@ -49,6 +52,9 @@ __all__ = [
|
|||||||
"DynamicChoiceField",
|
"DynamicChoiceField",
|
||||||
"ThemableCheckboxInput",
|
"ThemableCheckboxInput",
|
||||||
"ThemableCheckboxSelectMultiple",
|
"ThemableCheckboxSelectMultiple",
|
||||||
|
"ThemableChoiceField",
|
||||||
|
"ThemableDynamicChoiceField",
|
||||||
|
"ThemableSelectWidget",
|
||||||
"IPField",
|
"IPField",
|
||||||
"IPv4",
|
"IPv4",
|
||||||
"IPv6",
|
"IPv6",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
@ -23,9 +24,12 @@ from django.core import urlresolvers
|
|||||||
from django.forms import fields
|
from django.forms import fields
|
||||||
from django.forms.utils import flatatt # noqa
|
from django.forms.utils import flatatt # noqa
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
|
from django.template import Context # noqa
|
||||||
|
from django.template.loader import get_template # noqa
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import Promise # noqa
|
from django.utils.functional import Promise # noqa
|
||||||
from django.utils import html
|
from django.utils import html
|
||||||
|
from django.utils.safestring import mark_safe # noqa
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
|
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
|
||||||
@ -164,7 +168,7 @@ class SelectWidget(widgets.Select):
|
|||||||
....
|
....
|
||||||
....
|
....
|
||||||
|
|
||||||
widget=forms.SelectWidget( attrs={'class': 'switchable',
|
widget=forms.ThemableSelect( attrs={'class': 'switchable',
|
||||||
'data-slug': 'source'},
|
'data-slug': 'source'},
|
||||||
transform_html_attrs=get_title )
|
transform_html_attrs=get_title )
|
||||||
|
|
||||||
@ -187,26 +191,84 @@ class SelectWidget(widgets.Select):
|
|||||||
other_html = (u' selected="selected"'
|
other_html = (u' selected="selected"'
|
||||||
if option_value in selected_choices else '')
|
if option_value in selected_choices else '')
|
||||||
|
|
||||||
if callable(self.transform_html_attrs):
|
other_html += self.transform_option_html_attrs(option_label)
|
||||||
html_attrs = self.transform_html_attrs(option_label)
|
|
||||||
other_html += flatatt(html_attrs)
|
|
||||||
|
|
||||||
|
data_attr_html = self.get_data_attrs(option_label)
|
||||||
|
if data_attr_html:
|
||||||
|
other_html += ' ' + data_attr_html
|
||||||
|
|
||||||
|
option_label = self.transform_option_label(option_label)
|
||||||
|
|
||||||
|
return u'<option value="%s"%s>%s</option>' % (
|
||||||
|
html.escape(option_value), other_html, option_label)
|
||||||
|
|
||||||
|
def get_data_attrs(self, option_label):
|
||||||
|
other_html = []
|
||||||
if not isinstance(option_label, (six.string_types, Promise)):
|
if not isinstance(option_label, (six.string_types, Promise)):
|
||||||
for data_attr in self.data_attrs:
|
for data_attr in self.data_attrs:
|
||||||
data_value = html.conditional_escape(
|
data_value = html.conditional_escape(
|
||||||
force_text(getattr(option_label,
|
force_text(getattr(option_label,
|
||||||
data_attr, "")))
|
data_attr, "")))
|
||||||
other_html += ' data-%s="%s"' % (data_attr, data_value)
|
other_html.append('data-%s="%s"' % (data_attr, data_value))
|
||||||
|
return ' '.join(other_html)
|
||||||
|
|
||||||
if callable(self.transform):
|
def transform_option_label(self, option_label):
|
||||||
option_label = self.transform(option_label)
|
if (not isinstance(option_label, (six.string_types, Promise)) and
|
||||||
|
callable(self.transform)):
|
||||||
|
option_label = self.transform(option_label)
|
||||||
|
return html.conditional_escape(force_text(option_label))
|
||||||
|
|
||||||
return u'<option value="%s"%s>%s</option>' % (
|
def transform_option_html_attrs(self, option_label):
|
||||||
html.escape(option_value), other_html,
|
if not callable(self.transform_html_attrs):
|
||||||
html.conditional_escape(force_text(option_label)))
|
return ''
|
||||||
|
return flatatt(self.transform_html_attrs(option_label))
|
||||||
|
|
||||||
|
|
||||||
class DynamicSelectWidget(widgets.Select):
|
class ThemableSelectWidget(SelectWidget):
|
||||||
|
"""Bootstrap base select field widget."""
|
||||||
|
def render(self, name, value, attrs=None, choices=()):
|
||||||
|
# NOTE(woodnt): Currently the "attrs" contents are being added to the
|
||||||
|
# select that's hidden. It's unclear whether this is the
|
||||||
|
# desired behavior. In some cases, the attribute should
|
||||||
|
# remain solely on the now-hidden select. But in others
|
||||||
|
# if it should live on the bootstrap button (visible)
|
||||||
|
# or both.
|
||||||
|
|
||||||
|
new_choices = []
|
||||||
|
for opt_value, opt_label in itertools.chain(self.choices, choices):
|
||||||
|
other_html = self.transform_option_html_attrs(opt_label)
|
||||||
|
|
||||||
|
data_attr_html = self.get_data_attrs(opt_label)
|
||||||
|
if data_attr_html:
|
||||||
|
other_html += ' ' + data_attr_html
|
||||||
|
|
||||||
|
opt_label = self.transform_option_label(opt_label)
|
||||||
|
|
||||||
|
if other_html:
|
||||||
|
new_choices.append((opt_value, opt_label, other_html))
|
||||||
|
else:
|
||||||
|
new_choices.append((opt_value, opt_label))
|
||||||
|
|
||||||
|
initial_value = value
|
||||||
|
if value is None and new_choices:
|
||||||
|
initial_value = new_choices[0][1]
|
||||||
|
|
||||||
|
attrs = self.build_attrs(attrs)
|
||||||
|
id = attrs.pop('id', 'id_%s' % name)
|
||||||
|
|
||||||
|
template = get_template('horizon/common/fields/_themable_select.html')
|
||||||
|
context = Context({
|
||||||
|
'name': name,
|
||||||
|
'options': new_choices,
|
||||||
|
'id': id,
|
||||||
|
'value': value,
|
||||||
|
'initial_value': initial_value,
|
||||||
|
'select_attrs': attrs,
|
||||||
|
})
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicSelectWidget(SelectWidget):
|
||||||
"""A subclass of the ``Select`` widget which renders extra attributes for
|
"""A subclass of the ``Select`` widget which renders extra attributes for
|
||||||
use in callbacks to handle dynamic changes to the available choices.
|
use in callbacks to handle dynamic changes to the available choices.
|
||||||
"""
|
"""
|
||||||
@ -231,6 +293,15 @@ class DynamicSelectWidget(widgets.Select):
|
|||||||
return self.add_item_link
|
return self.add_item_link
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableDynamicSelectWidget(ThemableSelectWidget, DynamicSelectWidget):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableChoiceField(fields.ChoiceField):
|
||||||
|
"""Bootstrap based select field."""
|
||||||
|
widget = ThemableSelectWidget
|
||||||
|
|
||||||
|
|
||||||
class DynamicChoiceField(fields.ChoiceField):
|
class DynamicChoiceField(fields.ChoiceField):
|
||||||
"""A subclass of ``ChoiceField`` with additional properties that make
|
"""A subclass of ``ChoiceField`` with additional properties that make
|
||||||
dynamically updating its elements easier.
|
dynamically updating its elements easier.
|
||||||
@ -251,6 +322,10 @@ class DynamicChoiceField(fields.ChoiceField):
|
|||||||
self.widget.add_item_link_args = add_item_link_args
|
self.widget.add_item_link_args = add_item_link_args
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableDynamicChoiceField(DynamicChoiceField):
|
||||||
|
widget = ThemableDynamicSelectWidget
|
||||||
|
|
||||||
|
|
||||||
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
||||||
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
||||||
pass
|
pass
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% elif filter.filter_type == 'server' %}
|
{% elif filter.filter_type == 'server' %}
|
||||||
<div class="table_search">
|
<div class="table_search">
|
||||||
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options value=filter.filter_field %}
|
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options stand_alone=1 value=filter.filter_field %}
|
||||||
{% include 'horizon/common/fields/_themable_select.html' %}
|
{% include 'horizon/common/fields/_themable_select.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
|
<input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% load horizon %}
|
{% load horizon %}
|
||||||
|
|
||||||
{% minifyspace %}
|
{% minifyspace %}
|
||||||
<div class="themable-select dropdown" xmlns="http://www.w3.org/1999/html">
|
<div class="themable-select dropdown {% if not stand_alone %} form-control{% endif %}" xmlns="http://www.w3.org/1999/html">
|
||||||
<button type="button" class="btn btn-default dropdown-toggle"
|
<button type="button" class="btn btn-default dropdown-toggle"
|
||||||
data-toggle="dropdown"{% if value %}
|
data-toggle="dropdown"{% if value %}
|
||||||
title="{{ value }}" {% endif %}
|
title="{{ value }}" {% endif %}
|
||||||
@ -38,9 +38,7 @@
|
|||||||
name="{{ name }}"
|
name="{{ name }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for k,v in select_attrs.items %}
|
{% for k,v in select_attrs.items %}
|
||||||
{% if k == 'class' %}
|
{% if k != 'class' %}
|
||||||
class="form-control {{ v }}"
|
|
||||||
{% else %}
|
|
||||||
{{ k|safe }}="{{ v }}"
|
{{ k|safe }}="{{ v }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
{% if show_attach %}
|
{% if show_attach %}
|
||||||
<h3>{% trans "Attach To Instance" %}</h3>
|
<h3>{% trans "Attach To Instance" %}</h3>
|
||||||
<fieldset>
|
|
||||||
{% include "horizon/common/_form_fields.html" %}
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
</fieldset>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -80,28 +80,29 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
||||||
attrs={'rows': 4}),
|
attrs={'rows': 4}),
|
||||||
label=_("Description"), required=False)
|
label=_("Description"), required=False)
|
||||||
volume_source_type = forms.ChoiceField(label=_("Volume Source"),
|
volume_source_type = forms.ChoiceField(
|
||||||
required=False,
|
label=_("Volume Source"),
|
||||||
widget=forms.Select(attrs={
|
required=False,
|
||||||
'class': 'switchable',
|
widget=forms.ThemableSelectWidget(attrs={
|
||||||
'data-slug': 'source'}))
|
'class': 'switchable',
|
||||||
|
'data-slug': 'source'}))
|
||||||
snapshot_source = forms.ChoiceField(
|
snapshot_source = forms.ChoiceField(
|
||||||
label=_("Use snapshot as a source"),
|
label=_("Use snapshot as a source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'snapshot-selector'},
|
attrs={'class': 'snapshot-selector'},
|
||||||
data_attrs=('size', 'name'),
|
data_attrs=('size', 'name'),
|
||||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||||
required=False)
|
required=False)
|
||||||
image_source = forms.ChoiceField(
|
image_source = forms.ChoiceField(
|
||||||
label=_("Use image as a source"),
|
label=_("Use image as a source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'image-selector'},
|
attrs={'class': 'image-selector'},
|
||||||
data_attrs=('size', 'name', 'min_disk'),
|
data_attrs=('size', 'name', 'min_disk'),
|
||||||
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
||||||
required=False)
|
required=False)
|
||||||
volume_source = forms.ChoiceField(
|
volume_source = forms.ChoiceField(
|
||||||
label=_("Use a volume as source"),
|
label=_("Use a volume as source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'image-selector'},
|
attrs={'class': 'image-selector'},
|
||||||
data_attrs=('size', 'name'),
|
data_attrs=('size', 'name'),
|
||||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||||
@ -109,7 +110,7 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
label=_("Type"),
|
label=_("Type"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'switched',
|
attrs={'class': 'switched',
|
||||||
'data-switch-on': 'source',
|
'data-switch-on': 'source',
|
||||||
'data-source-no_source_type': _('Type'),
|
'data-source-no_source_type': _('Type'),
|
||||||
@ -118,7 +119,7 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
availability_zone = forms.ChoiceField(
|
availability_zone = forms.ChoiceField(
|
||||||
label=_("Availability Zone"),
|
label=_("Availability Zone"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'switched',
|
attrs={'class': 'switched',
|
||||||
'data-switch-on': 'source',
|
'data-switch-on': 'source',
|
||||||
'data-source-no_source_type': _('Availability Zone'),
|
'data-source-no_source_type': _('Availability Zone'),
|
||||||
@ -419,9 +420,9 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
|
|
||||||
|
|
||||||
class AttachForm(forms.SelfHandlingForm):
|
class AttachForm(forms.SelfHandlingForm):
|
||||||
instance = forms.ChoiceField(label=_("Attach to Instance"),
|
instance = forms.ThemableChoiceField(label=_("Attach to Instance"),
|
||||||
help_text=_("Select an instance to "
|
help_text=_("Select an instance to "
|
||||||
"attach to."))
|
"attach to."))
|
||||||
|
|
||||||
device = forms.CharField(label=_("Device Name"),
|
device = forms.CharField(label=_("Device Name"),
|
||||||
widget=forms.TextInput(attrs={'placeholder':
|
widget=forms.TextInput(attrs={'placeholder':
|
||||||
@ -645,7 +646,7 @@ class UploadToImageForm(forms.SelfHandlingForm):
|
|||||||
attrs={'readonly': 'readonly'}))
|
attrs={'readonly': 'readonly'}))
|
||||||
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
||||||
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
||||||
widget=forms.Select(),
|
widget=forms.ThemableSelectWidget(),
|
||||||
required=False)
|
required=False)
|
||||||
force = forms.BooleanField(
|
force = forms.BooleanField(
|
||||||
label=pgettext_lazy("Force upload volume in in-use status to image",
|
label=pgettext_lazy("Force upload volume in in-use status to image",
|
||||||
@ -751,11 +752,11 @@ class RetypeForm(forms.SelfHandlingForm):
|
|||||||
name = forms.CharField(label=_('Volume Name'),
|
name = forms.CharField(label=_('Volume Name'),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'readonly': 'readonly'}))
|
attrs={'readonly': 'readonly'}))
|
||||||
volume_type = forms.ChoiceField(label=_('Type'))
|
volume_type = forms.ThemableChoiceField(label=_('Type'))
|
||||||
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
||||||
('on-demand', _('On Demand'))]
|
('on-demand', _('On Demand'))]
|
||||||
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
||||||
widget=forms.Select(),
|
widget=forms.ThemableSelectWidget(),
|
||||||
choices=(MIGRATION_POLICY_CHOICES),
|
choices=(MIGRATION_POLICY_CHOICES),
|
||||||
initial='never',
|
initial='never',
|
||||||
required=False)
|
required=False)
|
||||||
|
@ -37,17 +37,19 @@
|
|||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-danger {
|
&.btn-danger {
|
||||||
color: $brand-danger;
|
color: $brand-danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-warning {
|
&.btn-warning {
|
||||||
color: $brand-warning;
|
color: $brand-warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-info {
|
&.btn-info {
|
||||||
color: $brand-info;
|
color: $brand-info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@include dropdown-button('default', $dropdown-link-color, $dropdown-link-hover-bg);
|
@include dropdown-button('default', $dropdown-link-color, $dropdown-link-hover-bg);
|
||||||
@include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color);
|
@include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color);
|
||||||
@include dropdown-button('info', $btn-info-bg, $btn-info-bg, $btn-info-color);
|
@include dropdown-button('info', $btn-info-bg, $btn-info-bg, $btn-info-color);
|
||||||
|
@ -3,15 +3,72 @@
|
|||||||
// ---------------------------------
|
// ---------------------------------
|
||||||
|
|
||||||
.themable-select {
|
.themable-select {
|
||||||
.dropdown-title {
|
position: relative;
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&:after {
|
.dropdown-title {
|
||||||
content: ' ';
|
text-align: left;
|
||||||
}
|
|
||||||
|
&:after {
|
||||||
|
content: ' ';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
display: none;
|
&,
|
||||||
|
&.form-control {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The dropdowns within forms should look like all other
|
||||||
|
// form elements
|
||||||
|
.form-group .themable-select {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: $input-bg;
|
||||||
|
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
|
||||||
|
border-color: $input-border;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
li > a {
|
||||||
|
@include text-overflow();
|
||||||
|
color: $input-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $dropdown-link-hover-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For vertical forms, we'll want the button to take on the entire width.
|
||||||
|
// think old-school launch-instance.
|
||||||
|
form:not(.form-inline) .form-group .themable-select {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
@extend .btn-block;
|
||||||
|
|
||||||
|
.fa-caret-down {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-title {
|
||||||
|
@include text-overflow();
|
||||||
|
width: calc(100% - 1em);
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
@ -228,7 +228,7 @@ class VolumesPage(basepage.BaseNavigationPage):
|
|||||||
|
|
||||||
|
|
||||||
class VolumeAttachForm(forms.BaseFormRegion):
|
class VolumeAttachForm(forms.BaseFormRegion):
|
||||||
_attach_to_instance_selector = (By.CSS_SELECTOR, 'select[name="instance"]')
|
_attach_to_instance_selector = (By.CSS_SELECTOR, 'div > .themable-select')
|
||||||
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
|
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
|
||||||
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
|
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
|
||||||
|
|
||||||
@ -239,7 +239,9 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
|||||||
@property
|
@property
|
||||||
def instance_selector(self):
|
def instance_selector(self):
|
||||||
src_elem = self._get_element(*self._attach_to_instance_selector)
|
src_elem = self._get_element(*self._attach_to_instance_selector)
|
||||||
return forms.SelectFormFieldRegion(self.driver, self.conf, src_elem)
|
return forms.ThemableSelectFormFieldRegion(
|
||||||
|
self.driver, self.conf, src_elem=src_elem,
|
||||||
|
strict_options_match=False)
|
||||||
|
|
||||||
def detach(self, volume, instance):
|
def detach(self, volume, instance):
|
||||||
detach_button = self.attachments_table.find_element(
|
detach_button = self.attachments_table.find_element(
|
||||||
@ -248,9 +250,5 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
|||||||
return forms.BaseFormRegion(self.driver, self.conf)
|
return forms.BaseFormRegion(self.driver, self.conf)
|
||||||
|
|
||||||
def attach_instance(self, instance_name):
|
def attach_instance(self, instance_name):
|
||||||
instance = filter(lambda x: x.startswith(instance_name),
|
self.instance_selector.text = instance_name
|
||||||
self.instance_selector.options.values())
|
|
||||||
if not instance:
|
|
||||||
raise AttributeError("Unable to select {0}".format(instance_name))
|
|
||||||
self.instance_selector.text = instance[0]
|
|
||||||
self.submit()
|
self.submit()
|
||||||
|
@ -36,7 +36,7 @@ class FieldFactory(baseregion.BaseRegion):
|
|||||||
field_cls._element_locator_str_suffix))
|
field_cls._element_locator_str_suffix))
|
||||||
elements = super(FieldFactory, self)._get_elements(*locator)
|
elements = super(FieldFactory, self)._get_elements(*locator)
|
||||||
for element in elements:
|
for element in elements:
|
||||||
yield field_cls(self.driver, self.conf, element)
|
yield field_cls(self.driver, self.conf, src_elem=element)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_field_cls(cls, field_class, base_classes=None):
|
def register_field_cls(cls, field_class, base_classes=None):
|
||||||
@ -174,7 +174,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
|
|||||||
class SelectFormFieldRegion(BaseFormFieldRegion):
|
class SelectFormFieldRegion(BaseFormFieldRegion):
|
||||||
"""Select box field."""
|
"""Select box field."""
|
||||||
|
|
||||||
_element_locator_str_suffix = 'div > select'
|
_element_locator_str_suffix = 'div > select.form-control'
|
||||||
|
|
||||||
def is_displayed(self):
|
def is_displayed(self):
|
||||||
return self.element._el.is_displayed()
|
return self.element._el.is_displayed()
|
||||||
@ -218,6 +218,67 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
|
|||||||
self.element.select_by_value(value)
|
self.element.select_by_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
|
||||||
|
"""Select box field."""
|
||||||
|
|
||||||
|
_element_locator_str_suffix = 'div > .themable-select'
|
||||||
|
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
|
||||||
|
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
|
||||||
|
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
|
||||||
|
|
||||||
|
def __init__(self, driver, conf, strict_options_match=True, **kwargs):
|
||||||
|
super(ThemableSelectFormFieldRegion, self).__init__(
|
||||||
|
driver, conf, **kwargs)
|
||||||
|
self.strict_options_match = strict_options_match
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden_element(self):
|
||||||
|
elem = self._get_element(*self._raw_select_locator)
|
||||||
|
return SelectFormFieldRegion(self.driver, self.conf, src_elem=elem)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.hidden_element.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._get_element(*self._selected_label_locator).text.strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.hidden_element.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
return self._get_elements(*self._dropdown_menu_locator)
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, text):
|
||||||
|
if text != self.text:
|
||||||
|
self.src_elem.click()
|
||||||
|
for option in self.options:
|
||||||
|
if self.strict_options_match:
|
||||||
|
match = text == option.text.strip()
|
||||||
|
else:
|
||||||
|
match = option.text.startswith(text)
|
||||||
|
if match:
|
||||||
|
option.click()
|
||||||
|
return
|
||||||
|
raise ValueError('Widget "%s" does have an option with text "%s"'
|
||||||
|
% (self.name, text))
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
if value != self.value:
|
||||||
|
self.src_elem.click()
|
||||||
|
for option in self.options:
|
||||||
|
if value == option.get_attribute('data-select-value'):
|
||||||
|
option.click()
|
||||||
|
return
|
||||||
|
raise ValueError('Widget "%s" does have an option with value "%s"'
|
||||||
|
% (self.name, value))
|
||||||
|
|
||||||
|
|
||||||
class BaseFormRegion(baseregion.BaseRegion):
|
class BaseFormRegion(baseregion.BaseRegion):
|
||||||
"""Base class for forms."""
|
"""Base class for forms."""
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
@import "components/navbar";
|
@import "components/navbar";
|
||||||
@import "components/pie_charts";
|
@import "components/pie_charts";
|
||||||
@import "components/quota";
|
@import "components/quota";
|
||||||
|
@import "components/selects";
|
||||||
@import "components/sidebar";
|
@import "components/sidebar";
|
||||||
@import "components/tables";
|
@import "components/tables";
|
||||||
@import "components/table_actions";
|
@import "components/table_actions";
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.form-group .themable-select .dropdown-menu {
|
||||||
|
@extend .dropdown-menu-right;
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
@import "components/magic_search";
|
@import "components/magic_search";
|
||||||
@import "components/messages";
|
@import "components/messages";
|
||||||
@import "components/navbar";
|
@import "components/navbar";
|
||||||
|
@import "components/selects";
|
||||||
@import "components/sidebar";
|
@import "components/sidebar";
|
||||||
|
|
||||||
.login .splash-logo {
|
.login .splash-logo {
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
// The dropdowns within forms should look like all other
|
||||||
|
// form elements, so this mimics Bootswatch's Paper's
|
||||||
|
// bootswatch.scss form-group styles
|
||||||
|
|
||||||
|
.form-group .themable-select {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&.active,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
@include box-shadow(inset 0 -1px 0 $table-border-color);
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@include box-shadow(inset 0 -2px 0 $brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&[readonly] {
|
||||||
|
@include box-shadow(none);
|
||||||
|
border-bottom: 1px dotted $table-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
&-sm {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-lg {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open .dropdown-menu {
|
||||||
|
background-color: $dropdown-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin material-select-states($state, $color) {
|
||||||
|
.form-group.#{$state} .themable-select {
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&.active,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
border-bottom: none;
|
||||||
|
@include box-shadow(inset 0 -2px 0 $color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include material-select-states('has-warning', $brand-warning);
|
||||||
|
@include material-select-states('has-error', $brand-danger);
|
||||||
|
@include material-select-states('has-success', $brand-success);
|
Loading…
Reference in New Issue
Block a user