summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRadomir Dopieralski <openstack@sheep.art.pl>2013-10-10 09:14:45 +0200
committerLadislav Smola <lsmola@redhat.com>2013-10-16 13:00:38 +0000
commitd4362da947c41937f069739d207c40e58826611a (patch)
tree23c824574018e6cd6e6d1fbce779cc0148100bb5
parentdb5901ae25882a6f07b2bb5717b07b337f054139 (diff)
downloadtuskar-ui-d4362da947c41937f069739d207c40e58826611a.tar.gz
Allow multiple nodes in a rack
Use a FormsetDataTable in Rack's create and edit workflows to allow creating or updating multiple nodes at once. This is a stopgap solution needed before we completely rework this workflow according to the new wireframes. There is a problem left: * There is currently no way to update the existing nodes in nova baremetal, so editing the fields of existing nodes has no effect. Closes-Bug: #1234245 Implements: blueprint rack-nodes-1-to-many Change-Id: I27f5e9df97af6ba49acf9702fac17a0a883c6930
-rw-r--r--tuskar_ui/infrastructure/resource_management/nodes/forms.py88
-rw-r--r--tuskar_ui/infrastructure/resource_management/nodes/tables.py34
-rw-r--r--tuskar_ui/infrastructure/resource_management/racks/tables.py1
-rw-r--r--tuskar_ui/infrastructure/resource_management/racks/tabs.py2
-rw-r--r--tuskar_ui/infrastructure/resource_management/racks/tests.py77
-rw-r--r--tuskar_ui/infrastructure/resource_management/racks/workflows.py192
-rw-r--r--tuskar_ui/infrastructure/resource_management/templates/resource_management/racks/_rack_nodes_step.html17
-rw-r--r--tuskar_ui/infrastructure/resource_management/urls.py6
-rw-r--r--tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less16
9 files changed, 321 insertions, 112 deletions
diff --git a/tuskar_ui/infrastructure/resource_management/nodes/forms.py b/tuskar_ui/infrastructure/resource_management/nodes/forms.py
new file mode 100644
index 00000000..550b6ba8
--- /dev/null
+++ b/tuskar_ui/infrastructure/resource_management/nodes/forms.py
@@ -0,0 +1,88 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import django.forms
+from django.utils.translation import ugettext_lazy as _ # noqa
+
+import tuskar_ui.forms
+
+
+class NodeForm(django.forms.Form):
+ id = django.forms.IntegerField(required=False,
+ widget=django.forms.HiddenInput())
+
+ service_host = django.forms.CharField(label=_("Service Host"),
+ widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
+ required=True)
+ mac_address = django.forms.CharField(label=_("MAC Address"),
+ widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
+ required=True)
+
+ # Hardware Specifications
+ cpus = django.forms.IntegerField(label=_("CPUs"), required=True,
+ min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
+ 'class': 'input number_input_slim',
+ }))
+ memory_mb = django.forms.IntegerField(label=_("Memory"),
+ required=True, min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
+ 'class': 'input number_input_slim',
+ }))
+ local_gb = django.forms.IntegerField(label=_("Local Disk (GB)"),
+ min_value=1, widget=tuskar_ui.forms.NumberInput(attrs={
+ 'class': 'input number_input_slim',
+ }), required=True)
+
+ # Power Management
+ pm_address = django.forms.GenericIPAddressField(
+ widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
+ label=_("Power Management IP"), required=False)
+ pm_user = django.forms.CharField(label=_("Power Management User"),
+ widget=django.forms.TextInput(attrs={'class': 'input input-mini'}),
+ required=False)
+ pm_password = django.forms.CharField(label=_("Power Management Password"),
+ required=False, widget=django.forms.PasswordInput(render_value=False,
+ attrs={'class': 'input input-mini'}))
+
+ # Access
+ terminal_port = django.forms.IntegerField(label=_("Terminal Port"),
+ required=False, min_value=0, max_value=1024,
+ widget=tuskar_ui.forms.NumberInput(attrs={
+ 'class': 'input number_input_slim',
+ }))
+
+
+class BaseNodeFormSet(django.forms.formsets.BaseFormSet):
+ def clean(self):
+ if any(self.errors):
+ # Don't bother validating the formset unless each form is valid
+ # on its own
+ return
+ unique_fields = ('mac_address', 'pm_address')
+ values = dict((field, {}) for field in unique_fields)
+ for form in self.forms:
+ for field in unique_fields:
+ value = form.cleaned_data.get(field)
+ if not value:
+ continue
+ if value in values[field]:
+ message = _("This value repeats, but it should be unique.")
+ other_form = values[field][value]
+ form._errors[field] = form.error_class([message])
+ other_form._errors[field] = other_form.error_class(
+ [message])
+ values[field][value] = form
+
+
+NodeFormset = django.forms.formsets.formset_factory(NodeForm, extra=1,
+ can_delete=True, formset=BaseNodeFormSet)
diff --git a/tuskar_ui/infrastructure/resource_management/nodes/tables.py b/tuskar_ui/infrastructure/resource_management/nodes/tables.py
index b4615cde..c815e705 100644
--- a/tuskar_ui/infrastructure/resource_management/nodes/tables.py
+++ b/tuskar_ui/infrastructure/resource_management/nodes/tables.py
@@ -17,6 +17,9 @@ from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import tables
from tuskar_ui import api as tuskar
+from tuskar_ui.infrastructure.resource_management.nodes import forms \
+ as nodes_forms
+import tuskar_ui.tables
class DeleteNodes(tables.DeleteAction):
@@ -47,7 +50,7 @@ class NodesTable(tables.DataTable):
verbose_name=_("Usage"))
class Meta:
- name = "nodes"
+ name = "nodes_table"
verbose_name = _("Nodes")
table_actions = (DeleteNodes, NodesFilterAction)
row_actions = (DeleteNodes,)
@@ -60,3 +63,32 @@ class UnrackedNodesTable(NodesTable):
verbose_name = _("Unracked Nodes")
table_actions = ()
row_actions = ()
+
+
+class NodesFormsetTable(tuskar_ui.tables.FormsetDataTable):
+ service_host = tables.Column('service_host', verbose_name=_("Name"))
+ mac_address = tables.Column('mac_address', verbose_name=_("MAC Address"))
+
+ cpus = tables.Column('cpus', verbose_name=_("CPUs"))
+ memory_mb = tables.Column('memory_mb', verbose_name=_("Memory (MB)"))
+ local_gb = tables.Column('local_gb', verbose_name=_("Local Disk (GB)"))
+
+ pm_address = tables.Column('pm_address',
+ verbose_name=_("Power Management IP"))
+ pm_user = tables.Column('pm_user', verbose_name=_("Power Management User"))
+ pm_password = tables.Column('pm_password',
+ verbose_name=_("Power Management Password"))
+
+ terminal_port = tables.Column('terminal_port',
+ verbose_name=_("Terminal Port"))
+
+ # This is needed for the formset with can_delete=True
+ DELETE = tables.Column('DELETE', verbose_name=_("Delete"))
+
+ formset_class = nodes_forms.NodeFormset
+
+ class Meta:
+ name = "nodes"
+ verbose_name = _("Nodes")
+ table_actions = ()
+ multi_select = False
diff --git a/tuskar_ui/infrastructure/resource_management/racks/tables.py b/tuskar_ui/infrastructure/resource_management/racks/tables.py
index 651a94ac..64e1f1c5 100644
--- a/tuskar_ui/infrastructure/resource_management/racks/tables.py
+++ b/tuskar_ui/infrastructure/resource_management/racks/tables.py
@@ -20,6 +20,7 @@ from horizon import tables
from tuskar_ui import api as tuskar
+
LOG = logging.getLogger(__name__)
diff --git a/tuskar_ui/infrastructure/resource_management/racks/tabs.py b/tuskar_ui/infrastructure/resource_management/racks/tabs.py
index 3782c93b..ccf2aab1 100644
--- a/tuskar_ui/infrastructure/resource_management/racks/tabs.py
+++ b/tuskar_ui/infrastructure/resource_management/racks/tabs.py
@@ -36,7 +36,7 @@ class NodesTab(tabs.TableTab):
slug = "nodes"
template_name = "horizon/common/_detail_table.html"
- def get_nodes_data(self):
+ def get_nodes_table_data(self):
try:
rack = self.tab_group.kwargs['rack']
nodes = rack.list_nodes
diff --git a/tuskar_ui/infrastructure/resource_management/racks/tests.py b/tuskar_ui/infrastructure/resource_management/racks/tests.py
index 62cdff95..639bd3c2 100644
--- a/tuskar_ui/infrastructure/resource_management/racks/tests.py
+++ b/tuskar_ui/infrastructure/resource_management/racks/tests.py
@@ -26,6 +26,10 @@ class RackViewTests(test.BaseAdminViewTests):
index_page = urlresolvers.reverse(
'horizon:infrastructure:resource_management:index')
+ index_page_racks_tab = (urlresolvers.reverse(
+ 'horizon:infrastructure:resource_management:index') +
+ "?tab=resource_management_tabs__racks_tab")
+
@test.create_stubs({tuskar.ResourceClass: ('list',)})
def test_create_rack_get(self):
tuskar.ResourceClass.list(
@@ -57,14 +61,14 @@ class RackViewTests(test.BaseAdminViewTests):
tuskar.BaremetalNode.create(
mox.IsA(http.HttpRequest),
name='New Node',
- cpus=u'1',
- memory_mb=u'1024',
- local_gb=u'10',
+ cpus=1,
+ memory_mb=1024,
+ local_gb=10,
prov_mac_address='aa:bb:cc:dd:ee',
pm_address=u'',
pm_user=u'',
pm_password=u'',
- terminal_port=u'').AndReturn(node)
+ terminal_port=None).AndReturn(node)
tuskar.Rack.create(
mox.IsA(http.HttpRequest),
name='New Rack',
@@ -78,14 +82,24 @@ class RackViewTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
- data = {'name': 'New Rack', 'resource_class_id': u'1',
- 'location': 'Tokyo', 'subnet': '1.2.3.4',
- 'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
- 'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
+ data = {
+ 'name': 'New Rack',
+ 'resource_class_id': u'1',
+ 'location': 'Tokyo',
+ 'subnet': '1.2.3.4',
+ 'nodes-TOTAL_FORMS': 1,
+ 'nodes-INITIAL_FORMS': 0,
+ 'nodes-MAX_NUM_FORMS': 1024,
+ 'nodes-0-service_host': 'New Node',
+ 'nodes-0-mac_address': 'aa:bb:cc:dd:ee',
+ 'nodes-0-cpus': u'1',
+ 'nodes-0-memory_mb': u'1024',
+ 'nodes-0-local_gb': u'10',
+ }
url = urlresolvers.reverse('horizon:infrastructure:'
'resource_management:racks:create')
resp = self.client.post(url, data)
- self.assertRedirectsNoFollow(resp, self.index_page)
+ self.assertRedirectsNoFollow(resp, self.index_page_racks_tab)
@test.create_stubs({tuskar.Rack: ('get', 'list_nodes'),
tuskar.ResourceClass: ('list',)})
@@ -94,39 +108,50 @@ class RackViewTests(test.BaseAdminViewTests):
tuskar.Rack.get(
mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
+ tuskar.Rack.list_nodes = []
+ tuskar.Rack.get(mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
tuskar.ResourceClass.list(
mox.IsA(http.HttpRequest)).AndReturn(
self.tuskar_resource_classes.list())
self.mox.ReplayAll()
- tuskar.Rack.list_nodes = []
-
url = urlresolvers.reverse('horizon:infrastructure:'
- 'resource_management:racks:edit',
- args=[1])
+ 'resource_management:racks:edit', args=[1])
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res,
- 'horizon/common/_workflow_base.html')
+ self.assertTemplateUsed(res, 'horizon/common/_workflow_base.html')
- @test.create_stubs({tuskar.Rack: ('get', 'list', 'update',),
+ @test.create_stubs({tuskar.Rack: ('get', 'list', 'update', 'list_nodes'),
tuskar.ResourceClass: ('list',)})
def test_edit_rack_post(self):
rack = self.tuskar_racks.first()
- rack_data = {'name': 'Updated Rack', 'resource_class_id': u'1',
- 'rack_id': u'1', 'location': 'New Location',
- 'subnet': '127.10.10.0/24', 'node_macs': None}
-
- data = {'name': 'Updated Rack', 'resource_class_id': u'1',
- 'rack_id': u'1', 'location': 'New Location',
- 'subnet': '127.10.10.0/24', 'node_macs': None,
- 'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
- 'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
+ rack_data = {
+ 'name': 'Updated Rack',
+ 'resource_class_id': u'1',
+ 'rack_id': u'1',
+ 'location': 'New Location',
+ 'subnet': '127.10.10.0/24',
+ 'nodes': [],
+ }
+
+ data = {
+ 'name': 'Updated Rack',
+ 'resource_class_id': u'1',
+ 'rack_id': u'1',
+ 'location': 'New Location',
+ 'subnet': '127.10.10.0/24',
+ 'nodes-TOTAL_FORMS': 0,
+ 'nodes-INITIAL_FORMS': 0,
+ 'nodes-MAX_NUM_FORMS': 1024,
+ }
tuskar.Rack.get(
mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
+ tuskar.Rack.list_nodes = []
+ tuskar.Rack.get(
+ mox.IsA(http.HttpRequest), rack.id).AndReturn(rack)
tuskar.Rack.list(
mox.IsA(http.HttpRequest)).AndReturn(
self.tuskar_racks.list())
@@ -143,7 +168,7 @@ class RackViewTests(test.BaseAdminViewTests):
response = self.client.post(url, data)
self.assertNoFormErrors(response)
self.assertMessageCount(success=1)
- self.assertRedirectsNoFollow(response, self.index_page)
+ self.assertRedirectsNoFollow(response, self.index_page_racks_tab)
@test.create_stubs({tuskar.Rack: ('get',)})
def test_edit_status_rack_get(self):
diff --git a/tuskar_ui/infrastructure/resource_management/racks/workflows.py b/tuskar_ui/infrastructure/resource_management/racks/workflows.py
index c004d751..ddd61a1a 100644
--- a/tuskar_ui/infrastructure/resource_management/racks/workflows.py
+++ b/tuskar_ui/infrastructure/resource_management/racks/workflows.py
@@ -23,55 +23,9 @@ from horizon import workflows
import requests
from tuskar_ui import api as tuskar
-
-
-class NodeCreateAction(workflows.Action):
- # node_macs = forms.CharField(label=_("MAC Addresses"),
- # widget=forms.Textarea(attrs={'rows': 12, 'cols': 20}),
- # required=False)
-
- node_name = forms.CharField(label="Name", required=True)
- prov_mac_address = forms.CharField(label=("MAC Address"),
- required=True)
-
- # Hardware Specifications
- cpus = forms.CharField(label="CPUs", required=True)
- memory_mb = forms.CharField(label="Memory", required=True)
- local_gb = forms.CharField(label="Local Disk (GB)", required=True)
-
- # Power Management
- pm_address = forms.CharField(label="Power Management IP", required=False)
- pm_user = forms.CharField(label="Power Management User", required=False)
- pm_password = forms.CharField(label="Power Management Password",
- required=False,
- widget=forms.PasswordInput(
- render_value=False))
-
- # Access
- terminal_port = forms.CharField(label="Terminal Port", required=False)
-
- class Meta:
- name = _("Nodes")
-
-
-# mawagner FIXME - For the demo, all we can really do is edit the one
-# associated node. That's very much _not_ what this form is actually
-# about, though.
-class NodeEditAction(NodeCreateAction):
-
- class Meta:
- name = _("Nodes")
-
- # FIXME: mawagner - This is all for debugging. The idea is to fetch
- # the first node and display it in the form; the latter part needs
- # implementation. This also needs error handling; right now for testing
- # I want to let it fail, but don't commit like that! :)
- def __init__(self, request, *args, **kwargs):
- super(NodeEditAction, self).__init__(request, *args, **kwargs)
- # TODO(Resolve node edits)
- #rack_id = self.initial['rack_id']
- #rack = tuskar.Rack.get(request, rack_id)
- #nodes = rack.list_nodes
+from tuskar_ui.infrastructure.resource_management.nodes import tables \
+ as nodes_tables
+import tuskar_ui.workflows
class RackCreateInfoAction(workflows.Action):
@@ -82,7 +36,6 @@ class RackCreateInfoAction(workflows.Action):
'contain letters, numbers, underscores, '
'periods and hyphens.')})
location = forms.CharField(label=_("Location"))
- # see GenericIPAddressField, but not for subnets:
subnet = forms.CharField(label=_("IP Subnet"))
resource_class_id = forms.ChoiceField(label=_("Resource Class"))
@@ -137,21 +90,52 @@ class EditRackInfo(CreateRackInfo):
depends_on = ('rack_id',)
-class CreateNodes(workflows.Step):
+class NodeCreateAction(workflows.Action):
+ class Meta:
+ name = _("Create Nodes")
+ help_text = _("Here you can create the nodes for this rack.")
+
+ def clean(self):
+ cleaned_data = super(NodeCreateAction, self).clean()
+ table = self.initial.get('_tables', {}).get('nodes')
+ if table:
+ formset = table.get_formset()
+ if formset.is_valid():
+ cleaned_data['nodes'] = [form.cleaned_data
+ for form in formset
+ if form.cleaned_data
+ and not
+ form.cleaned_data.get('DELETE')]
+ else:
+ raise forms.ValidationError(_("Errors in the nodes list."))
+ return cleaned_data
+
+
+class NodeEditAction(NodeCreateAction):
+ class Meta:
+ name = _("Edit Nodes")
+ help_text = _("Here you can edit the nodes for this rack.")
+
+
+class CreateNodes(tuskar_ui.workflows.TableStep):
action_class = NodeCreateAction
- contributes = ('node_name', 'prov_mac_address', 'cpus', 'memory_mb',
- 'local_gb', 'pm_address', 'pm_user', 'pm_password',
- 'terminal_port')
+ contributes = ('nodes',)
+ table_classes = (nodes_tables.NodesFormsetTable,)
+ template_name = (
+ 'infrastructure/resource_management/racks/_rack_nodes_step.html')
- def get_nodes_data():
- pass
+ def get_nodes_data(self):
+ return []
class EditNodes(CreateNodes):
action_class = NodeEditAction
depends_on = ('rack_id',)
- contributes = ('node_macs',)
- # help_text = _("Editing nodes via textbox is not presently supported.")
+
+ def get_nodes_data(self):
+ rack_id = self.workflow.context['rack_id']
+ rack = tuskar.Rack.get(self.workflow.request, rack_id)
+ return rack.list_nodes
class CreateRack(workflows.Workflow):
@@ -162,31 +146,75 @@ class CreateRack(workflows.Workflow):
success_message = _("Rack created.")
failure_message = _("Unable to create rack.")
+ # FIXME active tabs coflict
+ # When on page with tabs, the workflow with more steps is used,
+ # there is a conflict of active tabs and it always shows the
+ # first tab after an action. So I explicitly specify to what
+ # tab it should redirect after action, until the coflict will
+ # be fixed in Horizon.
+ def get_index_url(self):
+ """This url is used both as success and failure url"""
+ return "%s?tab=resource_management_tabs__racks_tab" %\
+ urlresolvers.reverse('horizon:infrastructure:resource_management:'
+ 'index')
+
+ def get_success_url(self):
+ return self.get_index_url()
+
+ def get_failure_url(self):
+ return self.get_index_url()
+
+ def create_or_update_node(self, node_data):
+ """Creates (if id=='') or updates (otherwise) a node."""
+ if node_data['id'] not in ('', None):
+ node_id = unicode(node_data['id'])
+ # TODO(rdopieralski) there is currently no way to update
+ # a baremetal node
+ #
+ # tuskar.BaremetalNode.update(
+ # self.request,
+ # node_id=node_id,
+ # name=node_data['service_host'],
+ # cpus=node_data['cpus'],
+ # memory_mb=node_data['memory_mb'],
+ # local_gb=node_data['local_gb'],
+ # prov_mac_address=node_data['mac_address'],
+ # pm_address=node_data['pm_address'],
+ # pm_user=node_data['pm_user'],
+ # pm_password=node_data['pm_password'],
+ # terminal_port=node_data['terminal_port'])
+ return node_id
+ else:
+ node = tuskar.BaremetalNode.create(
+ self.request,
+ name=node_data['service_host'],
+ cpus=node_data['cpus'],
+ memory_mb=node_data['memory_mb'],
+ local_gb=node_data['local_gb'],
+ prov_mac_address=node_data['mac_address'],
+ pm_address=node_data['pm_address'],
+ pm_user=node_data['pm_user'],
+ pm_password=node_data['pm_password'],
+ terminal_port=node_data['terminal_port'])
+ return node.id
+
def handle(self, request, data):
- try:
- if data['node_name'] is not None:
- node = tuskar.BaremetalNode.create(
- request,
- name=data['node_name'],
- cpus=data['cpus'],
- memory_mb=data['memory_mb'],
- local_gb=data['local_gb'],
- prov_mac_address=data['prov_mac_address'],
- pm_address=data['pm_address'],
- pm_user=data['pm_user'],
- pm_password=data['pm_password'],
- terminal_port=data['terminal_port'])
- if node:
- node_id = node.id
+ # First, create and/or update nodes
+ node_ids = []
+ for node_data in data['nodes']:
+ try:
+ node_id = self.create_or_update_node(node_data)
+ except Exception:
+ exceptions.handle(self.request, _("Unable to update node."))
+ return False
else:
- node_id = None
-
- # Then, register the Rack, including the node if it exists
+ node_ids.append({'id': node_id})
+ try:
+ # Then, register the Rack, including the nodes
tuskar.Rack.create(request, name=data['name'],
- resource_class_id=data['resource_class_id'],
- location=data['location'],
- subnet=data['subnet'],
- nodes=[{'id': node_id}])
+ resource_class_id=data['resource_class_id'],
+ location=data['location'], subnet=data['subnet'],
+ nodes=node_ids)
return True
except requests.ConnectionError:
@@ -208,12 +236,16 @@ class EditRack(CreateRack):
failure_message = _("Unable to update rack.")
def handle(self, request, data):
+ node_ids = [{'id': self.create_or_update_node(node_data)}
+ for node_data in data['nodes']]
try:
rack_id = self.context['rack_id']
+ data['nodes'] = node_ids
tuskar.Rack.update(request, rack_id, data)
return True
except Exception:
exceptions.handle(request, _("Unable to update rack."))
+ return False
class DetailEditRack(EditRack):
diff --git a/tuskar_ui/infrastructure/resource_management/templates/resource_management/racks/_rack_nodes_step.html b/tuskar_ui/infrastructure/resource_management/templates/resource_management/racks/_rack_nodes_step.html
new file mode 100644
index 00000000..3e0cbf1b
--- /dev/null
+++ b/tuskar_ui/infrastructure/resource_management/templates/resource_management/racks/_rack_nodes_step.html
@@ -0,0 +1,17 @@
+<noscript><h3>{{ step }}</h3></noscript>
+<table class="table-fixed">
+ <tbody>
+ <tr>
+ <td class="actions">
+ {% include "horizon/common/_form_fields.html" %}
+ </td>
+ <td class="help_text">
+ {{ step.get_help_text }}
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<div id="nodes-formset-datatable">
+ {{ nodes_table.render }}
+</div>
diff --git a/tuskar_ui/infrastructure/resource_management/urls.py b/tuskar_ui/infrastructure/resource_management/urls.py
index 2d1548cb..4f6052e3 100644
--- a/tuskar_ui/infrastructure/resource_management/urls.py
+++ b/tuskar_ui/infrastructure/resource_management/urls.py
@@ -39,7 +39,5 @@ urlpatterns = defaults.patterns('',
)
if conf.settings.DEBUG:
- urlpatterns += defaults.patterns('',
- defaults.url(r'^qunit$',
- defaults.include(test_urls, namespace='tests'))
- )
+ urlpatterns += defaults.patterns('', defaults.url(r'^qunit$',
+ defaults.include(test_urls, namespace='tests')))
diff --git a/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less b/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less
index 4b643b5b..3b55cc26 100644
--- a/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less
+++ b/tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less
@@ -508,3 +508,19 @@ input {
color: #3290c0;
}
}
+
+#nodes-formset-datatable .datatable tbody {
+ input {
+ padding: 2px 1px;
+ }
+ input.number_input_slim {
+ width: 3em;
+ }
+ td {
+ padding: 2px;
+ text-align: center;
+ a.close {
+ margin-right: 4px;
+ }
+ }
+}