New node registration and upload dialogs

According to the new wireframes.

The values for autodiscoverable parameters now default to empty values.

Change-Id: I1c6430cc0165c7df612c85c31e8ad17d82870b5c
This commit is contained in:
Radomir Dopieralski 2014-10-07 16:57:14 +02:00
parent 75e1ae05bf
commit db3d7cd35c
16 changed files with 247 additions and 283 deletions

View File

@ -109,7 +109,7 @@ class SelfHandlingFormset(forms.formsets.BaseFormSet):
def handle(self, request, data):
success = True
for form in self:
form_success = form.handle(request, data)
form_success = form.handle(request, form.cleaned_data)
if not form_success:
success = False
else:

View File

@ -11,7 +11,6 @@
# 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 csv
import django.forms
@ -24,6 +23,7 @@ import tuskar_ui.forms
CPU_ARCH_CHOICES = [
('', _("unspecified")),
('amd64', _("amd64")),
('x86', _("x86")),
('x86_64', _("x86_64")),
@ -36,9 +36,7 @@ DRIVER_CHOICES = [
def get_driver_info_dict(data):
driver = data['driver']
driver_dict = {
'driver': driver
}
driver_dict = {'driver': driver}
if driver == 'ipmi':
driver_dict.update(
# TODO(rdopieralski) If ipmi_address is no longer required,
@ -56,18 +54,22 @@ def get_driver_info_dict(data):
return driver_dict
def auto_discover_node(request, kwargs):
node = api.node.Node.create(
request,
**kwargs
def create_node(request, data):
kwargs = get_driver_info_dict(data)
kwargs.update(
cpu_arch=data.get('cpu_arch'),
cpus=data.get('cpus'),
memory_mb=data.get('memory_mb'),
local_gb=data.get('local_gb'),
mac_addresses=data['mac_addresses'].split(),
)
api.node.Node.set_maintenance(request,
node.uuid,
True)
node = api.node.Node.create(request, **kwargs)
if data.get('do_autodiscovery', False):
api.node.Node.set_maintenance(request, node.uuid, True)
api.node.Node.discover(request, [node.uuid])
class BaseNodeForm(django.forms.Form):
class NodeForm(django.forms.Form):
id = django.forms.IntegerField(
label="",
required=False,
@ -139,6 +141,46 @@ class BaseNodeForm(django.forms.Form):
'rows': 2,
}),
)
do_autodiscovery = django.forms.BooleanField(
label=_("Discover missing attributes"),
required=False,
)
mac_addresses = tuskar_ui.forms.MultiMACField(
label=_("NIC MAC Addresses"),
required=False,
widget=django.forms.Textarea(attrs={
'placeholder': _('unspecified'),
'rows': '2',
}),
)
cpu_arch = django.forms.ChoiceField(
label=_("Architecture"),
required=False,
choices=CPU_ARCH_CHOICES,
widget=django.forms.Select(
attrs={'placeholder': _('unspecified')}),
)
cpus = django.forms.IntegerField(
label=_("CPUs"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
memory_mb = django.forms.IntegerField(
label=_("Memory"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
local_gb = django.forms.IntegerField(
label=_("Local Disk"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
def get_name(self):
try:
@ -149,22 +191,10 @@ class BaseNodeForm(django.forms.Form):
name = _("Undefined node")
return name
def create_node(self, request, data):
kwargs = get_driver_info_dict(data)
kwargs.update(
cpu_arch=data.get('cpu_arch'),
cpus=data.get('cpus'),
memory_mb=data.get('memory_mb'),
local_gb=data.get('local_gb'),
mac_addresses=data['mac_addresses'].split(),
)
api.node.Node.create(request, **kwargs)
def handle(self, request, data):
success = True
data = self.cleaned_data
try:
self.create_node(request, data)
create_node(request, data)
except Exception:
success = False
exceptions.handle(request, _('Unable to register node.'))
@ -172,123 +202,68 @@ class BaseNodeForm(django.forms.Form):
# have to unregister nodes, delete ports, etc?
return success
def clean_ipmi_username(self):
return self.cleaned_data.get('ipmi_username') or None
class RegisterNodeForm(BaseNodeForm):
mac_addresses = tuskar_ui.forms.MultiMACField(
label=_("NIC MAC Addresses"),
widget=django.forms.Textarea(attrs={
'class': 'form-control',
'rows': '2',
}),
)
cpu_arch = django.forms.ChoiceField(
label=_("Architecture"),
required=True,
choices=CPU_ARCH_CHOICES,
widget=django.forms.Select(
attrs={'class': 'form-control'}),
)
cpus = django.forms.IntegerField(
label=_("CPUs"),
required=True,
min_value=1,
initial=1,
widget=tuskar_ui.forms.NumberInput(
attrs={'class': 'form-control'}),
)
memory_mb = django.forms.IntegerField(
label=_("Memory"),
required=True,
min_value=1,
initial=1,
widget=tuskar_ui.forms.NumberInput(
attrs={'class': 'form-control'}),
)
local_gb = django.forms.IntegerField(
label=_("Local Disk"),
required=True,
min_value=1,
initial=1,
widget=tuskar_ui.forms.NumberInput(
attrs={'class': 'form-control'}),
)
def clean_ipmi_password(self):
return self.cleaned_data.get('ipmi_password') or None
class AutoDiscoverNodeForm(BaseNodeForm):
mac_addresses = tuskar_ui.forms.MultiMACField(
label=_("NIC MAC Addresses"),
required=False,
widget=django.forms.Textarea(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ssh': _("PXE + SSH"),
'rows': 2,
}),
)
def create_node(self, request, data):
kwargs = get_driver_info_dict(data)
kwargs.update(
mac_addresses=data['mac_addresses'].split(),
)
auto_discover_node(request, kwargs)
def clean(self):
cleaned_data = super(NodeForm, self).clean()
if not cleaned_data.get('do_autodiscovery', False):
for field_name in [
'mac_addresses',
'cpu_arch',
'cpus',
'memory_mb',
'local_gb',
]:
if not cleaned_data.get(field_name):
self._errors[field_name] = self.error_class([(
u"This field is required "
u"when autodiscovery is disabled."
)])
return cleaned_data
class BaseNodeFormset(tuskar_ui.forms.SelfHandlingFormset):
def clean(self):
for form in self:
if not form.cleaned_data:
raise django.forms.ValidationError(
_("Please provide node data for all nodes."))
if not form.cleaned_data.get('ipmi_username'):
form.cleaned_data['ipmi_username'] = None
if not form.cleaned_data.get('ipmi_password'):
form.cleaned_data['ipmi_password'] = None
class UploadNodeForm(forms.SelfHandlingForm):
csv_file = forms.FileField(label=_("CSV File"), required=True)
def handle(self, request, data):
return True
def get_data(self):
data = []
for row in csv.reader(self.cleaned_data['csv_file']):
driver = row[0].strip()
if driver == 'pxe_ssh':
node = dict(
ssh_address=row[1],
ssh_username=row[2],
ssh_key_contents=row[3],
mac_addresses=row[4],
driver=driver,
do_autodiscovery=True,
)
elif driver == 'ipmi':
node = dict(
ipmi_address=row[1],
ipmi_username=row[2],
ipmi_password=row[3],
driver=driver,
do_autodiscovery=True,
)
data.append(node)
return data
RegisterNodeFormset = django.forms.formsets.formset_factory(
RegisterNodeForm, extra=1,
formset=BaseNodeFormset)
AutoDiscoverNodeFormset = django.forms.formsets.formset_factory(
AutoDiscoverNodeForm, extra=1,
formset=BaseNodeFormset)
class AutoDiscoverCSVNodeForm(forms.SelfHandlingForm):
csv_file = forms.FileField(label=_("CSV File"),
required=False)
def handle(self, request, data):
success = True
all_node_data = csv.reader(data['csv_file'])
for node_data in all_node_data:
driver = node_data[0]
kwargs = {
'driver': driver
}
if driver == 'pxe_ssh':
kwargs.update(
ssh_address=node_data[1],
ssh_username=node_data[2],
ssh_key_contents=node_data[3],
mac_addresses=node_data[4].split()
)
else:
kwargs.update(
ipmi_address=node_data[1],
ipmi_username=node_data[2],
ipmi_password=node_data[3],
)
try:
auto_discover_node(request, kwargs)
except Exception:
success = False
exceptions.handle(request, _('Unable to register node.'))
# TODO(tzumainn) If there is a failure between steps, do we
# have to unregister nodes, delete ports, etc?
return success
NodeForm, extra=1, formset=BaseNodeFormset)

View File

@ -1,20 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}autodiscover_nodes_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:nodes:auto-discover' %}{% endblock %}
{% block modal_id %}autodiscover_nodes_modal{% endblock %}
{% block modal-header %}{% trans "Auto-Discover Nodes" %}{% endblock %}
{% block modal-body %}
{% include "formset_table/menu_formset.html" with formset=form form_template="infrastructure/nodes/_auto_discover_nodes_formset_form.html" %}
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit"
value="{% trans "Register Nodes" %}" />
<a href="{% url 'horizon:infrastructure:nodes:index' %}"
class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -3,9 +3,10 @@
{% load url from future %}
{% block form_id %}autodiscover_csv_nodes_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:nodes:auto-discover-csv' %}{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:nodes:register' %}{% endblock %}
{% block modal_id %}autodiscover_csv_nodes_modal{% endblock %}
{% block modal-header %}{% trans "Auto-Discover Nodes (Upload CSV)" %}{% endblock %}
{% block modal-header %}{% trans "Upload Nodes" %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body %}
@ -13,9 +14,10 @@
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit"
value="{% trans "Register Nodes" %}" />
<button class="btn btn-primary pull-right" type="submit">
<i class="fa fa-upload"></i>
{% trans "Upload" %}
</button>
<a href="{% url 'horizon:infrastructure:nodes:index' %}"
class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,31 +0,0 @@
<div class="well tab-pane{% if active %} active{% endif %}"
id="tab-{{ form.prefix }}">
<div class="form form-inline"><fieldset>
<div class="row">
<h4>Node Detail</h4>
</div>
<div class="row">
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.driver required=True %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_address %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_username %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_password %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_address %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_username %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_key_contents %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_addresses %}
</div>
</fieldset></div>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
var form_prefix = '{{ form.prefix|escapejs }}';
var $form = $('#tab-' + form_prefix);
var $nav_link = $('a[href="#' + $form.attr('id') + '"]');
var undefined_name = '{{ form.get_name|escapejs }}';
$form.find('input[name$="-ipmi_address"]').change(function () {
$nav_link.html($(this).val() || undefined_name);
});
});
</script>

View File

@ -1,11 +1,14 @@
<div class="well tab-pane{% if active %} active{% endif %}"
{% load i18n %}
{% load form_helpers %}
<div class="container-fluid tab-pane{% if active %} active{% endif %}"
id="tab-{{ form.prefix }}">
<div class="form form-inline"><fieldset>
<div class="form form-inline"><fieldset class="well">
{% include 'horizon/common/_form_errors.html' with form=form %}
<div class="row">
<h4>Node Detail</h4>
<h4>{% trans "Node Detail" %}</h4>
</div>
<div class="row">
<h5>Power Management</h5>
<h5 class="row">{% trans "Power Management" %}</h5>
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.driver required=True %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_address %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ipmi_username %}
@ -13,20 +16,16 @@
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_address %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_username %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.ssh_key_contents %}
</div>
<div class="row">
<h5>Networking</h5>
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_addresses required=True %}
</div>
<div class="row">
<div class="col-xs-4">
<h5>Hardware</h5>
</div>
<label class="col-xs-6 checkbox checkbox-inline">
{{ form.introspect_hardware }}<small> {{ form.introspect_hardware.label }}</small>
<div class="panel panel-default">
<div class="panel-heading">
<label class="checkbox">
{{ form.do_autodiscovery|add_bootstrap_class }}
{{ form.do_autodiscovery.label }}
</label>
</div>
<div class="row" id="register-hardware-fields">
<h5 class="row">{% trans "Networking" %}</h5>
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.mac_addresses required=True %}
<h5 class="row">{% trans "Hardware" %}</h5>
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.cpu_arch required=True %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.cpus extra_text=_('units') required=True %}
{% include 'infrastructure/nodes/_nodes_formset_field.html' with field=form.memory_mb extra_text=_('MB') required=True %}
@ -45,5 +44,11 @@
$form.find('input[name$="-ipmi_address"]').change(function () {
$nav_link.html($(this).val() || undefined_name);
});
$form.find('input[name$="-do_autodiscovery"]').change(function () {
var $this = $(this);
$this.closest('.panel').find(
'.form-group .row').toggleClass('required', !($this.attr('checked')));
});
});
</script>

View File

@ -0,0 +1,24 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}upload_nodes_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:nodes:register' %}?upload{% endblock %}
{% block modal_id %}upload_nodes_modal{% endblock %}
{% block modal-header %}{% trans "Upload Nodes" %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body %}
{% include "horizon/common/_form_fields.html" %}
{% endblock %}
{% block modal-footer %}
<button class="btn btn-primary pull-right" type="submit">
<i class="fa fa-upload"></i>
{% trans "Upload" %}
</button>
<a href="{% url 'horizon:infrastructure:nodes:index' %}"
class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "infrastructure/base.html" %}
{% load i18n %}
{% block title %}{% trans "Auto-Discover Nodes" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Auto-Discover Nodes") %}
{% endblock %}
{% block main %}
{% include "infrastructure/nodes/_auto_discover.html" %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "infrastructure/base.html" %}
{% load i18n %}
{% block title %}{% trans "Auto-Discover Nodes (Upload CSV)" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Auto-Discover Nodes (Upload CSV)") %}
{% endblock %}
{% block main %}
{% include "infrastructure/nodes/_auto_discover_csv.html" %}
{% endblock %}

View File

@ -11,18 +11,16 @@
<div class="row">
<div class="col-xs-12">
<div class="actions pull-right">
<a href="{% url 'horizon:infrastructure:nodes:register' %}" class="btn btn-primary ajax-modal">
<a href="{% url 'horizon:infrastructure:nodes:register' %}"
class="btn btn-primary ajax-modal">
<span class="fa fa-plus"></span>
{% trans 'Register Nodes' %}
</a>
{% if ironic_enabled %}
<a href="{% url 'horizon:infrastructure:nodes:auto-discover' %}" class="btn btn-primary ajax-modal">
<span class="fa fa-search-plus"></span>
{% trans 'Auto-Discover Nodes' %}
</a>
<a href="{% url 'horizon:infrastructure:nodes:auto-discover-csv' %}" class="btn btn-primary ajax-modal">
<span class="fa fa-search-plus"></span>
{% trans 'Auto-Discover Nodes (Upload CSV)' %}
<a href="{% url 'horizon:infrastructure:nodes:auto-discover-csv' %}"
class="btn btn-primary ajax-modal">
<span class="fa fa-upload"></span>
{% trans 'Upload Nodes' %}
</a>
{% endif %}
</div>

View File

@ -0,0 +1,11 @@
{% extends "infrastructure/base.html" %}
{% load i18n %}
{% block title %}{% trans "Upload Nodes" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Upload Nodes") %}
{% endblock %}
{% block main %}
{% include "infrastructure/nodes/_upload.html" %}
{% endblock %}

View File

@ -17,7 +17,7 @@ import json
from django.core import urlresolvers
from horizon import exceptions as horizon_exceptions
from mock import patch, call # noqa
from mock import patch, call, ANY # noqa
from openstack_dashboard.test import helpers
from openstack_dashboard.test.test_data import utils
@ -157,11 +157,11 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
'create.return_value': node,
}) as Node:
res = self.client.post(REGISTER_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
request = Node.create.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Node.create.call_args_list, [
call(
request,
ANY,
ipmi_address=u'127.0.0.1',
cpu_arch='x86',
cpus=1,
@ -173,7 +173,7 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
driver='ipmi',
),
call(
request,
ANY,
ipmi_address=u'127.0.0.2',
cpu_arch='x86',
cpus=4,
@ -216,10 +216,9 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
}) as Node:
res = self.client.post(REGISTER_URL, data)
self.assertEqual(res.status_code, 200)
request = Node.create.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Node.create.call_args_list, [
call(
request,
ANY,
ipmi_address=u'127.0.0.1',
cpu_arch='x86',
cpus=1,
@ -231,7 +230,7 @@ class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
driver='ipmi',
),
call(
request,
ANY,
ipmi_address=u'127.0.0.2',
cpu_arch='x86',
cpus=4,

View File

@ -22,9 +22,7 @@ urlpatterns = urls.patterns(
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^register/$', views.RegisterView.as_view(),
name='register'),
urls.url(r'^auto-discover/$', views.AutoDiscoverView.as_view(),
name='auto-discover'),
urls.url(r'^auto-discover-csv/$', views.AutoDiscoverCSVView.as_view(),
urls.url(r'^auto-discover-csv/$', views.UploadView.as_view(),
name='auto-discover-csv'),
urls.url(r'^nodes_performance/$',
views.PerformanceView.as_view(), name='nodes_performance'),

View File

@ -14,9 +14,11 @@
import json
from django.core.urlresolvers import reverse_lazy
from django import http
import django.forms
import django.http
from django.utils.translation import ugettext_lazy as _
from django.views.generic import base
from horizon import exceptions
from horizon import forms as horizon_forms
from horizon import tabs as horizon_tabs
from horizon.utils import memoized
@ -50,32 +52,35 @@ class RegisterView(horizon_forms.ModalFormView):
return []
def get_form(self, form_class):
return form_class(self.request.POST or None,
initial=self.get_data(),
prefix=self.form_prefix)
initial = []
if self.request.FILES:
csv_form = forms.UploadNodeForm(self.request,
files=self.request.FILES)
if csv_form.is_valid():
initial = csv_form.get_data()
formset = forms.RegisterNodeFormset(
None,
initial=initial,
prefix=self.form_prefix,
)
formset.extra = 0
return formset
return forms.RegisterNodeFormset(
self.request.POST or None,
initial=initial,
prefix=self.form_prefix,
)
class AutoDiscoverView(horizon_forms.ModalFormView):
form_class = forms.AutoDiscoverNodeFormset
form_prefix = 'auto_discover_nodes'
template_name = 'infrastructure/nodes/auto_discover.html'
class UploadView(horizon_forms.ModalFormView):
form_class = forms.UploadNodeForm
template_name = 'infrastructure/nodes/upload.html'
success_url = reverse_lazy(
'horizon:infrastructure:nodes:index')
def get_data(self):
return []
def get_form(self, form_class):
return form_class(self.request.POST or None,
initial=self.get_data(),
prefix=self.form_prefix)
class AutoDiscoverCSVView(horizon_forms.ModalFormView):
form_class = forms.AutoDiscoverCSVNodeForm
template_name = 'infrastructure/nodes/auto_discover_csv.html'
success_url = reverse_lazy(
'horizon:infrastructure:nodes:index')
def post(self, request, *args, **kwargs):
# This form's POST is handled in RegisterView.
raise exceptions.NotFound()
class DetailView(horizon_tabs.TabView):
@ -138,5 +143,5 @@ class PerformanceView(base.TemplateView):
date_from=date_from, date_to=date_to,
stats_attr=stats_attr, barchart=barchart)
return http.HttpResponse(json.dumps(json_output),
content_type='application/json')
return django.http.HttpResponse(
json.dumps(json_output), content_type='application/json')

View File

@ -32,7 +32,7 @@ tuskar.menu_formset = (function () {
function add_delete_link($nav_item) {
var $form = $content.find($nav_item.find('a').attr('href'));
$nav_item.prepend('<span class="btn-small pull-right delete-icon"><i class="fa fa-trash"></i></span>');
$nav_item.prepend('<span class="btn-small pull-right delete-icon"><i class="fa fa-times"></i></span>');
$nav_item.find('span.delete-icon:first').click(function () {
var count;
$form.remove();
@ -53,13 +53,24 @@ tuskar.menu_formset = (function () {
$nav.append('<li><a href="#' + id + '" data-toggle="tab">Undefined node</a></li>');
$new_nav = $nav.find('li > a:last');
add_delete_link($new_nav.parent());
$new_nav.click(function () { $(this).tab('show'); });
$new_nav.click(function () {
$(this).tab('show');
$('select.switchable').trigger('change');
});
$new_nav.tab('show');
$('select.switchable').trigger('change');
}
// Connect all signals.
$('a.add-node-link').click(add_node);
$nav.find('li').each(function () { add_delete_link($(this)); });
$nav.find('li').each(function () {
add_delete_link($(this));
});
$nav.find('li a').click(function () {
window.setTimeout(function () {
$('select.switchable').trigger('change');
}, 0);
});
// Activate the first field that has errors.
$content.find('.control-group.error').each(function () {
@ -68,6 +79,7 @@ tuskar.menu_formset = (function () {
activated = true;
}
});
};
return module;

View File

@ -78,10 +78,18 @@ $link-color: #428bca;
font-weight: normal;
}
.panel {
margin: 8px -8px 8px -8px;
padding: 8px;
.panel-heading {
margin: -8px -8px 0 -8px;
}
}
fieldset .form-group {
width: 100%;
input, textarea, select {
width: 150px;
width: 100%;
margin-left: 5px;
}
}