A dialog for registering nodes
A dialog for registering new nodes, as described by the wireframe. Implements: blueprint tripleo-node-registration-widget Change-Id: I6c4566677a3ba5c2de0c6994d30133e1fdd5a60d
This commit is contained in:
parent
7712bc059f
commit
378c296f85
@ -25,8 +25,8 @@ class MACField(forms.fields.Field):
|
||||
def clean(self, value):
|
||||
try:
|
||||
return str(netaddr.EUI(
|
||||
value, version=48, dialect=netaddr.mac_unix)).upper()
|
||||
except netaddr.AddrFormatError:
|
||||
value.strip(), version=48, dialect=netaddr.mac_unix)).upper()
|
||||
except (netaddr.AddrFormatError, TypeError):
|
||||
raise forms.ValidationError(_(u'Enter a valid MAC address.'))
|
||||
|
||||
|
||||
|
127
tuskar_ui/infrastructure/nodes/forms.py
Normal file
127
tuskar_ui/infrastructure/nodes/forms.py
Normal file
@ -0,0 +1,127 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# 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 _
|
||||
|
||||
from horizon import exceptions
|
||||
from tuskar_ui import api
|
||||
import tuskar_ui.forms
|
||||
|
||||
|
||||
class NodeForm(django.forms.Form):
|
||||
id = django.forms.IntegerField(
|
||||
label="",
|
||||
required=False,
|
||||
widget=django.forms.HiddenInput(),
|
||||
)
|
||||
|
||||
ip_address = django.forms.IPAddressField(
|
||||
label=_("IP Address"),
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
ipmi_user = django.forms.CharField(
|
||||
label=_("IPMI User"),
|
||||
required=False,
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
ipmi_password = django.forms.CharField(
|
||||
label=_("IPMI Password"),
|
||||
required=False,
|
||||
widget=django.forms.PasswordInput(
|
||||
render_value=False, attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
|
||||
mac_address = tuskar_ui.forms.MACField(
|
||||
label=_("NIC MAC Address"),
|
||||
widget=django.forms.Textarea(attrs={
|
||||
'class': 'input input-medium',
|
||||
'rows': 2,
|
||||
}),
|
||||
)
|
||||
|
||||
ipmi_user = django.forms.CharField(
|
||||
label=_("IPMI User"),
|
||||
required=False,
|
||||
widget=django.forms.TextInput(attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
ipmi_password = django.forms.CharField(
|
||||
label=_("IPMI Password"),
|
||||
required=False,
|
||||
widget=django.forms.PasswordInput(
|
||||
render_value=False, attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
cpus = django.forms.IntegerField(
|
||||
label=_("CPUs"),
|
||||
required=True,
|
||||
min_value=1,
|
||||
initial=1,
|
||||
widget=tuskar_ui.forms.NumberInput(
|
||||
attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
memory = django.forms.IntegerField(
|
||||
label=_("Memory"),
|
||||
required=True,
|
||||
min_value=1,
|
||||
initial=1,
|
||||
widget=tuskar_ui.forms.NumberInput(
|
||||
attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
local_disk = django.forms.IntegerField(
|
||||
label=_("Local Disk"),
|
||||
required=True,
|
||||
min_value=1,
|
||||
initial=1,
|
||||
widget=tuskar_ui.forms.NumberInput(
|
||||
attrs={'class': 'input input-medium'}),
|
||||
)
|
||||
|
||||
def get_name(self):
|
||||
try:
|
||||
name = self.fields['ip_address'].value()
|
||||
except AttributeError:
|
||||
# when the field is not bound
|
||||
name = _("Undefined node")
|
||||
return name
|
||||
|
||||
|
||||
class BaseNodeFormset(django.forms.formsets.BaseFormSet):
|
||||
def handle(self, request, data):
|
||||
success = True
|
||||
for form in self:
|
||||
try:
|
||||
api.Node.create(
|
||||
request,
|
||||
form.cleaned_data['ip_address'],
|
||||
form.cleaned_data.get('cpus'),
|
||||
form.cleaned_data.get('memory'),
|
||||
form.cleaned_data.get('local_disk'),
|
||||
[form.cleaned_data['mac_address']],
|
||||
form.cleaned_data.get('ipmi_username'),
|
||||
form.cleaned_data.get('ipmi_password'),
|
||||
)
|
||||
except Exception:
|
||||
success = False
|
||||
exceptions.handle(request, _('Unable to register node.'))
|
||||
# TODO(rdopieralski) Somehow find out if any port creation
|
||||
# failed and remove the mac addresses that succeeded from
|
||||
# the form.
|
||||
else:
|
||||
# TODO(rdopieralski) Remove successful nodes from formset.
|
||||
pass
|
||||
return success
|
||||
|
||||
|
||||
NodeFormset = django.forms.formsets.formset_factory(NodeForm, extra=1,
|
||||
formset=BaseNodeFormset)
|
107
tuskar_ui/infrastructure/nodes/overview/tests.py
Normal file
107
tuskar_ui/infrastructure/nodes/overview/tests.py
Normal file
@ -0,0 +1,107 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# 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.
|
||||
|
||||
from django.core import urlresolvers
|
||||
|
||||
from mock import patch, call # noqa
|
||||
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
from tuskar_ui.test import helpers as test
|
||||
from tuskar_ui.test.test_data import tuskar_data
|
||||
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:nodes.overview:index')
|
||||
REGISTER_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:nodes.overview:register')
|
||||
TEST_DATA = utils.TestDataContainer()
|
||||
tuskar_data.data(TEST_DATA)
|
||||
|
||||
|
||||
class RegisterNodesTests(test.BaseAdminViewTests):
|
||||
def test_index_get(self):
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/nodes/overview/index.html')
|
||||
|
||||
def test_register_get(self):
|
||||
res = self.client.get(REGISTER_URL)
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/nodes/overview/register.html')
|
||||
|
||||
def test_register_post(self):
|
||||
node = TEST_DATA.ironicclient_nodes.first
|
||||
data = {
|
||||
'register_nodes-TOTAL_FORMS': 2,
|
||||
'register_nodes-INITIAL_FORMS': 1,
|
||||
'register_nodes-MAX_NUM_FORMS': 1000,
|
||||
|
||||
'register_nodes-0-ip_address': '127.0.0.1',
|
||||
'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe',
|
||||
'register_nodes-0-cpus': '1',
|
||||
'register_nodes-0-memory': '2',
|
||||
'register_nodes-0-local_disk': '3',
|
||||
|
||||
'register_nodes-1-ip_address': '127.0.0.2',
|
||||
'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff',
|
||||
'register_nodes-1-cpus': '4',
|
||||
'register_nodes-1-memory': '5',
|
||||
'register_nodes-1-local_disk': '6',
|
||||
}
|
||||
with patch('tuskar_ui.api.Node', **{
|
||||
'spec_set': ['create'],
|
||||
'create.return_value': node,
|
||||
}) as Node:
|
||||
res = self.client.post(REGISTER_URL, data)
|
||||
request = Node.create.call_args_list[0][0][0] # This is a hack.
|
||||
self.assertListEqual(Node.create.call_args_list, [
|
||||
call(request, '127.0.0.1', 1, 2, 3,
|
||||
['DE:AD:BE:EF:CA:FE'], None, u''),
|
||||
call(request, '127.0.0.2', 4, 5, 6,
|
||||
['DE:AD:BE:EF:CA:FF'], None, u''),
|
||||
])
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_register_post_exception(self):
|
||||
data = {
|
||||
'register_nodes-TOTAL_FORMS': 2,
|
||||
'register_nodes-INITIAL_FORMS': 1,
|
||||
'register_nodes-MAX_NUM_FORMS': 1000,
|
||||
|
||||
'register_nodes-0-ip_address': '127.0.0.1',
|
||||
'register_nodes-0-mac_address': 'de:ad:be:ef:ca:fe',
|
||||
'register_nodes-0-cpus': '1',
|
||||
'register_nodes-0-memory': '2',
|
||||
'register_nodes-0-local_disk': '3',
|
||||
|
||||
'register_nodes-1-ip_address': '127.0.0.2',
|
||||
'register_nodes-1-mac_address': 'de:ad:be:ef:ca:ff',
|
||||
'register_nodes-1-cpus': '4',
|
||||
'register_nodes-1-memory': '5',
|
||||
'register_nodes-1-local_disk': '6',
|
||||
}
|
||||
with patch('tuskar_ui.api.Node', **{
|
||||
'spec_set': ['create'],
|
||||
'create.side_effect': self.exceptions.tuskar,
|
||||
}) as Node:
|
||||
res = self.client.post(REGISTER_URL, data)
|
||||
request = Node.create.call_args_list[0][0][0] # This is a hack.
|
||||
self.assertListEqual(Node.create.call_args_list, [
|
||||
call(request, '127.0.0.1', 1, 2, 3,
|
||||
['DE:AD:BE:EF:CA:FE'], None, u''),
|
||||
call(request, '127.0.0.2', 4, 5, 6,
|
||||
['DE:AD:BE:EF:CA:FF'], None, u''),
|
||||
])
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/nodes/overview/register.html')
|
@ -1,4 +1,4 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# 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
|
||||
@ -12,12 +12,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import defaults
|
||||
from django.conf import urls
|
||||
|
||||
from tuskar_ui.infrastructure.nodes.overview import views
|
||||
|
||||
|
||||
urlpatterns = defaults.patterns(
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
defaults.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^register/$', views.RegisterView.as_view(),
|
||||
name='register'),
|
||||
)
|
||||
|
@ -11,8 +11,29 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.views import generic
|
||||
import horizon.forms
|
||||
|
||||
from tuskar_ui.infrastructure.nodes import forms
|
||||
|
||||
|
||||
class IndexView(generic.TemplateView):
|
||||
template_name = 'infrastructure/base.html'
|
||||
template_name = 'infrastructure/nodes/overview/index.html'
|
||||
|
||||
|
||||
class RegisterView(horizon.forms.ModalFormView):
|
||||
form_class = forms.NodeFormset
|
||||
form_prefix = 'register_nodes'
|
||||
template_name = 'infrastructure/nodes/overview/register.html'
|
||||
success_url = reverse_lazy(
|
||||
'horizon:infrastructure:nodes.overview: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)
|
||||
|
107
tuskar_ui/infrastructure/static/bootstrap/less/variables.less
Normal file
107
tuskar_ui/infrastructure/static/bootstrap/less/variables.less
Normal file
@ -0,0 +1,107 @@
|
||||
// Variables.less
|
||||
// Variables to customize the look and feel of Bootstrap
|
||||
// -----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// GLOBAL VALUES
|
||||
// --------------------------------------------------
|
||||
|
||||
// Links
|
||||
@linkColor: #08c;
|
||||
@linkColorHover: darken(@linkColor, 15%);
|
||||
|
||||
// Grays
|
||||
@black: #000;
|
||||
@grayDarker: #222;
|
||||
@grayDark: #333;
|
||||
@gray: #555;
|
||||
@grayLight: #999;
|
||||
@grayLighter: #eee;
|
||||
@white: #fff;
|
||||
|
||||
// Accent colors
|
||||
@blue: #049cdb;
|
||||
@blueDark: #0064cd;
|
||||
@green: #46a546;
|
||||
@red: #9d261d;
|
||||
@yellow: #ffc40d;
|
||||
@orange: #f89406;
|
||||
@pink: #c3325f;
|
||||
@purple: #7a43b6;
|
||||
|
||||
// Typography
|
||||
@baseFontSize: 13px;
|
||||
@baseFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
@baseLineHeight: 18px;
|
||||
@textColor: @grayDark;
|
||||
|
||||
// Buttons
|
||||
@primaryButtonBackground: @linkColor;
|
||||
|
||||
|
||||
|
||||
// COMPONENT VARIABLES
|
||||
// --------------------------------------------------
|
||||
|
||||
// Z-index master list
|
||||
// Used for a bird's eye view of components dependent on the z-axis
|
||||
// Try to avoid customizing these :)
|
||||
@zindexDropdown: 1000;
|
||||
@zindexPopover: 1010;
|
||||
@zindexTooltip: 1020;
|
||||
@zindexFixedNavbar: 1030;
|
||||
@zindexModalBackdrop: 1040;
|
||||
@zindexModal: 1050;
|
||||
|
||||
// Sprite icons path
|
||||
@iconSpritePath: "/static/bootstrap/img/glyphicons-halflings.png";
|
||||
@iconWhiteSpritePath: "/static/bootstrap/img/glyphicons-halflings-white.png";
|
||||
|
||||
// Input placeholder text color
|
||||
@placeholderText: @grayLight;
|
||||
|
||||
// Hr border color
|
||||
@hrBorder: @grayLighter;
|
||||
|
||||
// Navbar
|
||||
@navbarHeight: 40px;
|
||||
@navbarBackground: @grayDarker;
|
||||
@navbarBackgroundHighlight: @grayDark;
|
||||
@navbarLinkBackgroundHover: transparent;
|
||||
|
||||
@navbarText: @grayLight;
|
||||
@navbarLinkColor: @grayLight;
|
||||
@navbarLinkColorHover: @white;
|
||||
|
||||
// Form states and alerts
|
||||
@warningText: #c09853;
|
||||
@warningBackground: #fcf8e3;
|
||||
@warningBorder: darken(spin(@warningBackground, -10), 3%);
|
||||
|
||||
@errorText: #b94a48;
|
||||
@errorBackground: #f2dede;
|
||||
@errorBorder: darken(spin(@errorBackground, -10), 3%);
|
||||
|
||||
@successText: #468847;
|
||||
@successBackground: #dff0d8;
|
||||
@successBorder: darken(spin(@successBackground, -10), 5%);
|
||||
|
||||
@infoText: #3a87ad;
|
||||
@infoBackground: #d9edf7;
|
||||
@infoBorder: darken(spin(@infoBackground, -10), 7%);
|
||||
|
||||
|
||||
|
||||
// GRID
|
||||
// --------------------------------------------------
|
||||
|
||||
// Default 940px grid
|
||||
@gridColumns: 12;
|
||||
@gridColumnWidth: 60px;
|
||||
@gridGutterWidth: 20px;
|
||||
@gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1));
|
||||
|
||||
// Fluid grid
|
||||
@fluidGridColumnWidth: 6.382978723%;
|
||||
@fluidGridGutterWidth: 2.127659574%;
|
@ -0,0 +1,74 @@
|
||||
tuskar.menu_formset = (function () {
|
||||
'use strict';
|
||||
|
||||
var module = {};
|
||||
|
||||
module.init = function (prefix, empty_form_html) {
|
||||
var input_name_re = new RegExp('^' + prefix + '-(\\d+|__prefix__)-');
|
||||
var input_id_re = new RegExp('^id_' + prefix + '-(\\d+|__prefix__)-');
|
||||
var $content = $('#formset-' + prefix +' .tab-content');
|
||||
var $nav = $('#formset-' + prefix + ' .nav');
|
||||
var activated = false;
|
||||
|
||||
function renumber_form($form, prefix, count) {
|
||||
$form.find('input, textarea, select').each(function () {
|
||||
var input = $(this);
|
||||
input.attr('name', input.attr('name').replace(
|
||||
input_name_re, prefix + '-' + count + '-'));
|
||||
input.attr('id', input.attr('id').replace(
|
||||
input_id_re, 'id_' + prefix + '-' + count + '-'));
|
||||
});
|
||||
}
|
||||
|
||||
function reenumerate_forms($forms, prefix) {
|
||||
var count = 0;
|
||||
$forms.each(function () {
|
||||
renumber_form($(this), prefix, count);
|
||||
count += 1;
|
||||
});
|
||||
$('#id_' + prefix + '-TOTAL_FORMS').val(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
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="icon-trash icon-white"></i></span>');
|
||||
$nav_item.find('span.delete-icon:first').click(function () {
|
||||
var count;
|
||||
$form.remove();
|
||||
$nav_item.remove();
|
||||
count = reenumerate_forms($content.find('.tab-pane'), prefix);
|
||||
if (count === 0) { add_node(); }
|
||||
});
|
||||
}
|
||||
|
||||
function add_node() {
|
||||
var $new_form = $(empty_form_html);
|
||||
var count, id, $new_nav;
|
||||
$content.append($new_form);
|
||||
$new_form = $content.find('.tab-pane:last');
|
||||
count = reenumerate_forms($content.find('.tab-pane'), prefix);
|
||||
id = 'tab-' + prefix + '-' + count;
|
||||
$new_form.attr('id', id);
|
||||
$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.tab('show');
|
||||
}
|
||||
|
||||
// Connect all signals.
|
||||
$('a.add-node-link').click(add_node);
|
||||
$nav.find('li').each(function () { add_delete_link($(this)); });
|
||||
|
||||
// Activate the first field that has errors.
|
||||
$content.find('.control-group.error').each(function () {
|
||||
if (!activated) {
|
||||
$nav.find('a[href="#' + $(this).closest('.tab-pane').attr('id') + '"]').tab('show');
|
||||
activated = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return module;
|
||||
} ());
|
@ -1,4 +1,5 @@
|
||||
/* Additional CSS for infrastructure. */
|
||||
@import "../../bootstrap/less/variables.less";
|
||||
|
||||
// global layout
|
||||
html,
|
||||
@ -524,3 +525,49 @@ input {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register nodes formset
|
||||
.register-nodes-formset {
|
||||
a.add-node-link {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.nav-tabs > .active > a {
|
||||
color: @white;
|
||||
background-color: @linkColor;
|
||||
}
|
||||
ul.nav-tabs > li span.delete-icon {
|
||||
display: none;
|
||||
}
|
||||
ul.nav-tabs > li.active span.delete-icon {
|
||||
display: block;
|
||||
margin: 4px 19px 4px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul.nav-tabs > li {
|
||||
position: relative;
|
||||
}
|
||||
ul.nav-tabs > li.active {
|
||||
width: 107%;
|
||||
}
|
||||
ul.nav-tabs > li.active:after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: -7px;
|
||||
border-top: 16px solid transparent;
|
||||
border-bottom: 16px solid transparent;
|
||||
border-left: 8px solid @linkColor;
|
||||
}
|
||||
.register-nav-head {
|
||||
margin-top: 19px;
|
||||
}
|
||||
.form h4, .form h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form label.checkbox {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
{% load i18n %}
|
||||
{{ formset.management_form }}
|
||||
<div class="row-fluid register-nodes-formset" id="formset-{{ formset.prefix }}">
|
||||
<div class="span5">
|
||||
<div class="clearfix register-nav-head">
|
||||
<a class="pull-right add-node-link"
|
||||
href="#"><i class="icon-plus"></i> {% trans "Add Node" %}</a>
|
||||
<h3>Nodes to register</h3>
|
||||
</div>
|
||||
<ul class="nav nav-tabs nav-stacked">
|
||||
{% for form in formset %}
|
||||
<li {% if forloop.first %}class="active"{% endif %}>
|
||||
<a href="#tab-{{ form.prefix }}" data-toggle="tab">
|
||||
{{ form.get_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="span7">
|
||||
<div class="tab-content">
|
||||
{% for form in formset %}
|
||||
{% include form_template with form=form active=forloop.first %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(window.$ || window.addHorizonLoadEvent)(function () {
|
||||
var prefix = '{{ formset.prefix|escapejs }}';
|
||||
var empty_form_html = '{% filter escapejs %}{% include form_template with form=formset.empty_form %}{% endfilter %}';
|
||||
tuskar.menu_formset.init(prefix, empty_form_html);
|
||||
});
|
||||
</script>
|
@ -6,6 +6,7 @@
|
||||
<script src='{{ STATIC_URL }}infrastructure/js/tuskar.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}infrastructure/js/tuskar.templates.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}infrastructure/js/tuskar.formset_table.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}infrastructure/js/tuskar.menu_formset.js' type='text/javascript' charset='utf-8'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% comment %} Tuskar-UI Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="row-fluid form-field control-group{% if field.errors %} error{% endif %}{% if required or field.required %} required{% endif %}">
|
||||
<label class="span4 control-label">{{ field.label }}</label>
|
||||
<div class="span6">{{ field }}</div>
|
||||
<div class="span2 muted">{{ extra_text|default:'' }}</div>
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
<div class="row-fluid form-field control-group error">
|
||||
{% for error in field.errors %}
|
||||
<span class="help-inline">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
@ -0,0 +1,45 @@
|
||||
<div class="well well-small tab-pane{% if active %} active{% endif %}"
|
||||
id="tab-{{ form.prefix }}">
|
||||
<div class="form form-inline">
|
||||
<div class="row-fluid">
|
||||
<h3 style="margin-bottom:16px">Node Detail</h3>
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.node_tags %}
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<h4>Power Management</h4>
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.ip_address required=True %}
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.ipmi_user %}
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.ipmi_password %}
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<h4>Networking</h4>
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.mac_address required=True %}
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<h4>Hardware</h4>
|
||||
</div>
|
||||
<label class="span6 checkbox checkbox-inline">
|
||||
{{ form.introspect_hardware }}<small> {{ form.introspect_hardware.label }}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row-fluid" id="register-hardware-fields">
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.cpus extra_text=_('units') required=True %}
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.memory extra_text=_('MB') required=True %}
|
||||
{% include 'infrastructure/nodes/overview/_nodes_formset_field.html' with field=form.local_disk extra_text=_('GB') required=True %}
|
||||
</div>
|
||||
</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$="-ip_address"]').change(function () {
|
||||
$nav_link.html($(this).val() || undefined_name);
|
||||
});
|
||||
});
|
||||
</script>
|
@ -0,0 +1,20 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}register_nodes_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:nodes.overview:register' %}{% endblock %}
|
||||
{% block modal_id %}register_nodes_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Register Nodes" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
{% include "formset_table/menu_formset.html" with formset=form form_template="infrastructure/nodes/overview/_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.overview:index' %}"
|
||||
class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,13 @@
|
||||
{% extends 'infrastructure/base.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
{% block title %}{% trans 'Nodes Overview' %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include 'horizon/common/_domain_page_header.html' with title=_('Nodes Overview') %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<a href="{% url 'horizon:infrastructure:nodes.overview:register' %}"
|
||||
class="btn ajax-modal">Register nodes</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Register Nodes" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Register Nodes") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "infrastructure/nodes/overview/_register.html" %}
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user